mirror of
https://github.com/openziti/zrok.git
synced 2026-08-24 10:14:56 -05:00
Merge pull request #1102 from openziti/striped_cookies
Striped Cookie Support (#1101)
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
.$*
|
||||
.idea
|
||||
.vscode
|
||||
.contexts
|
||||
CLAUDE.md
|
||||
*.db
|
||||
/automated-release-build/
|
||||
etc/dev.yml
|
||||
etc/dev-*
|
||||
etc/*.pem
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## v1.1.9
|
||||
|
||||
CHANGE: The `publicProxy` now supports "striped session cookies" to support larger authentication payloads when working with OIDC providers that use larger tokens/payloads. (https://github.com/openziti/zrok/issues/1101)
|
||||
|
||||
FIX: Fix for icon/favicon in HTML for the api console. (https://github.com/openziti/zrok/pull/1094)
|
||||
|
||||
## v1.1.8
|
||||
|
||||
@@ -20,7 +20,7 @@ type Frontend struct {
|
||||
}
|
||||
|
||||
func (str *Store) CreateFrontend(envId int, f *Frontend, tx *sqlx.Tx) (int, error) {
|
||||
stmt, err := tx.Prepare("insert into frontends (environment_id, private_share_id, token, z_id, public_name, url_template, reserved, permission_mode, description, bind_address) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning id")
|
||||
stmt, err := tx.Unsafe().Prepare("insert into frontends (environment_id, private_share_id, token, z_id, public_name, url_template, reserved, permission_mode, description, bind_address) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) returning id")
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "error preparing frontends insert statement")
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func (str *Store) CreateFrontend(envId int, f *Frontend, tx *sqlx.Tx) (int, erro
|
||||
}
|
||||
|
||||
func (str *Store) CreateGlobalFrontend(f *Frontend, tx *sqlx.Tx) (int, error) {
|
||||
stmt, err := tx.Prepare("insert into frontends (token, z_id, public_name, url_template, reserved, permission_mode, description) values ($1, $2, $3, $4, $5, $6, $7) returning id")
|
||||
stmt, err := tx.Unsafe().Prepare("insert into frontends (token, z_id, public_name, url_template, reserved, permission_mode, description) values ($1, $2, $3, $4, $5, $6, $7) returning id")
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "error preparing global frontends insert statement")
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func (str *Store) CreateGlobalFrontend(f *Frontend, tx *sqlx.Tx) (int, error) {
|
||||
|
||||
func (str *Store) GetFrontend(id int, tx *sqlx.Tx) (*Frontend, error) {
|
||||
i := &Frontend{}
|
||||
if err := tx.QueryRowx("select * from frontends where id = $1", id).StructScan(i); err != nil {
|
||||
if err := tx.Unsafe().QueryRowx("select * from frontends where id = $1", id).StructScan(i); err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting frontend by id")
|
||||
}
|
||||
return i, nil
|
||||
@@ -53,7 +53,7 @@ func (str *Store) GetFrontend(id int, tx *sqlx.Tx) (*Frontend, error) {
|
||||
|
||||
func (str *Store) FindFrontendWithToken(token string, tx *sqlx.Tx) (*Frontend, error) {
|
||||
i := &Frontend{}
|
||||
if err := tx.QueryRowx("select frontends.* from frontends where token = $1 and not deleted", token).StructScan(i); err != nil {
|
||||
if err := tx.Unsafe().QueryRowx("select frontends.* from frontends where token = $1 and not deleted", token).StructScan(i); err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting frontend by name")
|
||||
}
|
||||
return i, nil
|
||||
@@ -61,7 +61,7 @@ func (str *Store) FindFrontendWithToken(token string, tx *sqlx.Tx) (*Frontend, e
|
||||
|
||||
func (str *Store) FindFrontendWithZId(zId string, tx *sqlx.Tx) (*Frontend, error) {
|
||||
i := &Frontend{}
|
||||
if err := tx.QueryRowx("select frontends.* from frontends where z_id = $1 and not deleted", zId).StructScan(i); err != nil {
|
||||
if err := tx.Unsafe().QueryRowx("select frontends.* from frontends where z_id = $1 and not deleted", zId).StructScan(i); err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting frontend by ziti id")
|
||||
}
|
||||
return i, nil
|
||||
@@ -69,14 +69,14 @@ func (str *Store) FindFrontendWithZId(zId string, tx *sqlx.Tx) (*Frontend, error
|
||||
|
||||
func (str *Store) FindFrontendPubliclyNamed(publicName string, tx *sqlx.Tx) (*Frontend, error) {
|
||||
i := &Frontend{}
|
||||
if err := tx.QueryRowx("select frontends.* from frontends where public_name = $1 and not deleted", publicName).StructScan(i); err != nil {
|
||||
if err := tx.Unsafe().QueryRowx("select frontends.* from frontends where public_name = $1 and not deleted", publicName).StructScan(i); err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting frontend by public_name")
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (str *Store) FindFrontendsForEnvironment(envId int, tx *sqlx.Tx) ([]*Frontend, error) {
|
||||
rows, err := tx.Queryx("select frontends.* from frontends where environment_id = $1 and not deleted", envId)
|
||||
rows, err := tx.Unsafe().Queryx("select frontends.* from frontends where environment_id = $1 and not deleted", envId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting frontends by environment_id")
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (str *Store) FindFrontendsForEnvironment(envId int, tx *sqlx.Tx) ([]*Fronte
|
||||
}
|
||||
|
||||
func (str *Store) FindPublicFrontends(tx *sqlx.Tx) ([]*Frontend, error) {
|
||||
rows, err := tx.Queryx("select frontends.* from frontends where environment_id is null and reserved = true and not deleted")
|
||||
rows, err := tx.Unsafe().Queryx("select frontends.* from frontends where environment_id is null and reserved = true and not deleted")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting public frontends")
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func (str *Store) FindPublicFrontends(tx *sqlx.Tx) ([]*Frontend, error) {
|
||||
}
|
||||
|
||||
func (str *Store) FindOpenPublicFrontends(tx *sqlx.Tx) ([]*Frontend, error) {
|
||||
rows, err := tx.Queryx("select frontends.* from frontends where environment_id is null and permission_mode = 'open' and reserved = true and not deleted")
|
||||
rows, err := tx.Unsafe().Queryx("select frontends.* from frontends where environment_id is null and permission_mode = 'open' and reserved = true and not deleted")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting open public frontends")
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (str *Store) FindOpenPublicFrontends(tx *sqlx.Tx) ([]*Frontend, error) {
|
||||
}
|
||||
|
||||
func (str *Store) FindClosedPublicFrontendsGrantedToAccount(accountId int, tx *sqlx.Tx) ([]*Frontend, error) {
|
||||
rows, err := tx.Queryx(`
|
||||
rows, err := tx.Unsafe().Queryx(`
|
||||
select frontends.* from frontends
|
||||
inner join frontend_grants on frontends.id = frontend_grants.frontend_id
|
||||
where frontend_grants.account_id = $1
|
||||
@@ -148,7 +148,7 @@ func (str *Store) FindClosedPublicFrontendsGrantedToAccount(accountId int, tx *s
|
||||
}
|
||||
|
||||
func (str *Store) FindFrontendsForPrivateShare(shrId int, tx *sqlx.Tx) ([]*Frontend, error) {
|
||||
rows, err := tx.Queryx("select frontends.* from frontends where private_share_id = $1 and not deleted", shrId)
|
||||
rows, err := tx.Unsafe().Queryx("select frontends.* from frontends where private_share_id = $1 and not deleted", shrId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error selecting frontends by private_share_id")
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func (str *Store) FindFrontendsForPrivateShare(shrId int, tx *sqlx.Tx) ([]*Front
|
||||
|
||||
func (str *Store) UpdateFrontend(fe *Frontend, tx *sqlx.Tx) error {
|
||||
sql := "update frontends set environment_id = $1, private_share_id = $2, token = $3, z_id = $4, public_name = $5, url_template = $6, reserved = $7, permission_mode = $8, description = $9, bind_address = $10, updated_at = current_timestamp where id = $11"
|
||||
stmt, err := tx.Prepare(sql)
|
||||
stmt, err := tx.Unsafe().Prepare(sql)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error preparing frontends update statement")
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func (str *Store) UpdateFrontend(fe *Frontend, tx *sqlx.Tx) error {
|
||||
}
|
||||
|
||||
func (str *Store) DeleteFrontend(id int, tx *sqlx.Tx) error {
|
||||
stmt, err := tx.Prepare("update frontends set updated_at = current_timestamp, deleted = true where id = $1")
|
||||
stmt, err := tx.Unsafe().Prepare("update frontends set updated_at = current_timestamp, deleted = true where id = $1")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error preparing frontends delete statement")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// OAuthCookieConfig defines the interface for OAuth cookie configuration
|
||||
// This allows different proxy types to implement their own config structs while sharing cookie utilities
|
||||
type OAuthCookieConfig interface {
|
||||
GetCookieName() string
|
||||
GetCookieDomain() string
|
||||
GetMaxCookieSize() int
|
||||
GetSessionLifetime() time.Duration
|
||||
}
|
||||
|
||||
// CompressToken compresses a token string using gzip and returns base64-encoded result
|
||||
func CompressToken(token string) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
gzipWriter := gzip.NewWriter(&buf)
|
||||
if _, err := gzipWriter.Write([]byte(token)); err != nil {
|
||||
return "", fmt.Errorf("error writing to gzip writer: %w", err)
|
||||
}
|
||||
if err := gzipWriter.Close(); err != nil {
|
||||
return "", fmt.Errorf("error closing gzip writer: %w", err)
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
// DecompressToken decompresses a base64-encoded, gzip-compressed token string
|
||||
func DecompressToken(compressed string) (string, error) {
|
||||
data, err := base64.URLEncoding.DecodeString(compressed)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error decoding base64: %w", err)
|
||||
}
|
||||
|
||||
gzipReader, err := gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error creating gzip reader: %w", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
decompressed, err := io.ReadAll(gzipReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading from gzip reader: %w", err)
|
||||
}
|
||||
|
||||
return string(decompressed), nil
|
||||
}
|
||||
|
||||
// GetSessionCookie retrieves and reassembles a session cookie that may be striped across multiple cookies
|
||||
func GetSessionCookie(r *http.Request, cookieName string) (*http.Cookie, error) {
|
||||
baseCookie, err := r.Cookie(cookieName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check if this is a striped cookie by looking for the count prefix
|
||||
parts := strings.SplitN(baseCookie.Value, "|", 2)
|
||||
if len(parts) != 2 {
|
||||
// not striped, decompress and return
|
||||
decompressed, err := DecompressToken(baseCookie.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing cookie: %w", err)
|
||||
}
|
||||
return &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: decompressed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// striped cookie - reassemble all chunks
|
||||
count, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid cookie count prefix: %w", err)
|
||||
}
|
||||
|
||||
// start with the data from the base cookie
|
||||
var reassembled strings.Builder
|
||||
reassembled.WriteString(parts[1])
|
||||
|
||||
// retrieve and append the numbered chunks
|
||||
for i := 1; i < count; i++ {
|
||||
chunkName := fmt.Sprintf("%s_%d", cookieName, i)
|
||||
chunk, err := r.Cookie(chunkName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("missing cookie chunk %s: %w", chunkName, err)
|
||||
}
|
||||
reassembled.WriteString(chunk.Value)
|
||||
}
|
||||
|
||||
// decompress the reassembled data
|
||||
decompressed, err := DecompressToken(reassembled.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing reassembled cookie: %w", err)
|
||||
}
|
||||
|
||||
return &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: decompressed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetSessionCookie compresses and stripes a session cookie across multiple cookies if needed
|
||||
func SetSessionCookie(w http.ResponseWriter, cookieName string, tokenValue string, cfg OAuthCookieConfig) error {
|
||||
// compress the token
|
||||
compressed, err := CompressToken(tokenValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error compressing token: %w", err)
|
||||
}
|
||||
|
||||
maxSize := cfg.GetMaxCookieSize()
|
||||
if maxSize == 0 {
|
||||
maxSize = 3072
|
||||
}
|
||||
|
||||
// if compressed data fits in a single cookie, set it directly
|
||||
if len(compressed) <= maxSize {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: compressed,
|
||||
MaxAge: int(cfg.GetSessionLifetime().Seconds()),
|
||||
Domain: cfg.GetCookieDomain(),
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(cfg.GetSessionLifetime()),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// need to stripe across multiple cookies
|
||||
logrus.Debugf("cookie size %d exceeds max %d, striping across multiple cookies", len(compressed), maxSize)
|
||||
|
||||
// calculate how many cookies we need
|
||||
// account for the count prefix in the first cookie (e.g., "3|")
|
||||
countPrefixSize := len(fmt.Sprintf("%d|", (len(compressed)/maxSize)+2)) // estimate
|
||||
firstChunkSize := maxSize - countPrefixSize
|
||||
remainingSize := len(compressed) - firstChunkSize
|
||||
additionalChunks := (remainingSize + maxSize - 1) / maxSize // ceiling division
|
||||
totalCookies := additionalChunks + 1
|
||||
|
||||
// set the base cookie with count prefix
|
||||
firstChunkData := compressed[:firstChunkSize]
|
||||
baseValue := fmt.Sprintf("%d|%s", totalCookies, firstChunkData)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: baseValue,
|
||||
MaxAge: int(cfg.GetSessionLifetime().Seconds()),
|
||||
Domain: cfg.GetCookieDomain(),
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(cfg.GetSessionLifetime()),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
// set the numbered chunks
|
||||
offset := firstChunkSize
|
||||
for i := 1; i < totalCookies; i++ {
|
||||
chunkName := fmt.Sprintf("%s_%d", cookieName, i)
|
||||
end := offset + maxSize
|
||||
if end > len(compressed) {
|
||||
end = len(compressed)
|
||||
}
|
||||
chunkData := compressed[offset:end]
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: chunkName,
|
||||
Value: chunkData,
|
||||
MaxAge: int(cfg.GetSessionLifetime().Seconds()),
|
||||
Domain: cfg.GetCookieDomain(),
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(cfg.GetSessionLifetime()),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
offset = end
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearSessionCookies clears all session cookies including any striped chunks
|
||||
func ClearSessionCookies(w http.ResponseWriter, r *http.Request, cookieName string, cfg OAuthCookieConfig) {
|
||||
// iterate through all cookies and clear any that match the session cookie pattern
|
||||
for _, cookie := range r.Cookies() {
|
||||
// clear base cookie or any numbered chunks (cookieName_1, cookieName_2, etc.)
|
||||
if cookie.Name == cookieName || strings.HasPrefix(cookie.Name, cookieName+"_") {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookie.Name,
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Domain: cfg.GetCookieDomain(),
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FilterSessionCookies filters out session cookies (including striped chunks) from a cookie list
|
||||
func FilterSessionCookies(cookies []*http.Cookie, cookieName string) []*http.Cookie {
|
||||
var filtered []*http.Cookie
|
||||
for _, cookie := range cookies {
|
||||
// skip the base cookie
|
||||
if cookie.Name == cookieName {
|
||||
continue
|
||||
}
|
||||
// skip numbered chunks (e.g., "cookieName_1", "cookieName_2", etc.)
|
||||
if strings.HasPrefix(cookie.Name, cookieName+"_") {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, cookie)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package publicProxy
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// deriveKey uses HKDF to expand a "password" into a []byte to be used as a key; better than just a raw hash
|
||||
func deriveKey(keyString string, sz int) ([]byte, error) {
|
||||
// DeriveKey uses HKDF to expand a "password" into a []byte to be used as a key; better than just a raw hash
|
||||
func DeriveKey(keyString string, sz int) ([]byte, error) {
|
||||
out := hkdf.New(sha256.New, []byte(keyString), nil, []byte("derived-key"))
|
||||
key := make([]byte, sz)
|
||||
_, err := out.Read(key)
|
||||
@@ -19,9 +19,9 @@ func deriveKey(keyString string, sz int) ([]byte, error) {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// encryptToken uses AES-GCM (256) to encrypt tokens for inclusion in session tokens so that they're opaque outside of
|
||||
// EncryptToken uses AES-GCM (256) to encrypt tokens for inclusion in session tokens so that they're opaque outside of
|
||||
// the auth subsystem
|
||||
func encryptToken(token string, key []byte) (string, error) {
|
||||
func EncryptToken(token string, key []byte) (string, error) {
|
||||
enc, err := jose.NewEncrypter(
|
||||
jose.A256GCM,
|
||||
jose.Recipient{
|
||||
@@ -42,7 +42,8 @@ func encryptToken(token string, key []byte) (string, error) {
|
||||
return obj.CompactSerialize()
|
||||
}
|
||||
|
||||
func decryptToken(encrypted string, key []byte) (string, error) {
|
||||
// DecryptToken decrypts an encrypted token using AES-GCM (256)
|
||||
func DecryptToken(encrypted string, key []byte) (string, error) {
|
||||
obj, err := jose.ParseEncrypted(encrypted, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A256GCM})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse encrypted token: %v", err)
|
||||
@@ -47,7 +47,7 @@ func (h *authHandler) handleOAuth(w http.ResponseWriter, r *http.Request, cfg ma
|
||||
refreshInterval := getRefreshInterval(oauthMap)
|
||||
target := fmt.Sprintf("%s%s", r.Host, r.URL.Path)
|
||||
|
||||
cookie, err := r.Cookie(h.cfg.Oauth.CookieName)
|
||||
cookie, err := getSessionCookie(r, h.cfg.Oauth.CookieName)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to get '%v' cookie: %v", h.cfg.Oauth.CookieName, err)
|
||||
oauthLoginRequired(w, r, h.cfg.Oauth, provider, target, refreshInterval)
|
||||
|
||||
@@ -40,6 +40,7 @@ type OauthConfig struct {
|
||||
CookieDomain string
|
||||
SessionLifetime time.Duration
|
||||
IntermediateLifetime time.Duration
|
||||
MaxCookieSize int
|
||||
SigningKey string `cf:"+secret"`
|
||||
EncryptionKey string `cf:"+secret"`
|
||||
Providers []interface{} `cf:"+secret"`
|
||||
@@ -147,3 +148,8 @@ func configureOauth(ctx context.Context, cfg *Config, tls bool) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OauthConfig) GetCookieName() string { return c.CookieName }
|
||||
func (c *OauthConfig) GetCookieDomain() string { return c.CookieDomain }
|
||||
func (c *OauthConfig) GetMaxCookieSize() int { return c.MaxCookieSize }
|
||||
func (c *OauthConfig) GetSessionLifetime() time.Duration { return c.SessionLifetime }
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/openziti/zrok/endpoints"
|
||||
"github.com/openziti/zrok/endpoints/proxyUi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -23,6 +24,11 @@ type sessionCookieRequest struct {
|
||||
targetHost string
|
||||
}
|
||||
|
||||
// getSessionCookie retrieves and reassembles a session cookie using the shared endpoints package
|
||||
func getSessionCookie(r *http.Request, cookieName string) (*http.Cookie, error) {
|
||||
return endpoints.GetSessionCookie(r, cookieName)
|
||||
}
|
||||
|
||||
func setSessionCookie(w http.ResponseWriter, req sessionCookieRequest) {
|
||||
targetHost := strings.TrimSpace(req.targetHost)
|
||||
if targetHost == "" {
|
||||
@@ -33,7 +39,7 @@ func setSessionCookie(w http.ResponseWriter, req sessionCookieRequest) {
|
||||
}
|
||||
targetHost = strings.Split(targetHost, "/")[0]
|
||||
|
||||
encryptedAccessToken, err := encryptToken(req.accessToken, req.encryptionKey)
|
||||
encryptedAccessToken, err := endpoints.EncryptToken(req.accessToken, req.encryptionKey)
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to encrypt access token: %v", err)
|
||||
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("failed to encrypt access token")))
|
||||
@@ -59,30 +65,41 @@ func setSessionCookie(w http.ResponseWriter, req sessionCookieRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: req.oauthCfg.CookieName,
|
||||
Value: sTkn,
|
||||
MaxAge: int(req.oauthCfg.SessionLifetime.Seconds()),
|
||||
Domain: req.oauthCfg.CookieDomain,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(req.oauthCfg.SessionLifetime),
|
||||
// Secure: true, // pending server tls feature https://github.com/openziti/zrok/issues/24
|
||||
HttpOnly: true, // enabled because zrok frontend is the only intended consumer of this cookie, not client-side scripts
|
||||
SameSite: http.SameSiteLaxMode, // explicitly set to the default Lax mode which allows the zrok share to be navigated to from another site and receive the cookie
|
||||
})
|
||||
// use the shared endpoints package to set the cookie with compression and striping
|
||||
if err := endpoints.SetSessionCookie(w, req.oauthCfg.CookieName, sTkn, req.oauthCfg); err != nil {
|
||||
logrus.Errorf("failed to set session cookie: %v", err)
|
||||
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedUser(req.email).WithError(errors.New("failed to set session cookie")))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// clearSessionCookies clears all session cookies using the shared endpoints package
|
||||
func clearSessionCookies(w http.ResponseWriter, r *http.Request, cookieName string, cfg *OauthConfig) {
|
||||
endpoints.ClearSessionCookies(w, r, cookieName, cfg)
|
||||
}
|
||||
|
||||
// filterSessionCookies strips out the configured session cookie and also any `pkce` cookie
|
||||
func filterSessionCookies(w http.ResponseWriter, r *http.Request, cfg *Config) {
|
||||
cookies := r.Cookies()
|
||||
r.Header.Del("Cookie")
|
||||
for _, cookie := range cookies {
|
||||
if cfg.Oauth != nil && cfg.Oauth.CookieName == cookie.Name {
|
||||
continue
|
||||
|
||||
if cfg.Oauth != nil {
|
||||
// use the shared endpoints package to filter session cookies
|
||||
filtered := endpoints.FilterSessionCookies(cookies, cfg.Oauth.CookieName)
|
||||
for _, cookie := range filtered {
|
||||
// also filter out pkce cookie
|
||||
if cookie.Name == "pkce" {
|
||||
continue
|
||||
}
|
||||
r.AddCookie(cookie)
|
||||
}
|
||||
if cookie.Name == "pkce" {
|
||||
continue
|
||||
} else {
|
||||
// no oauth config, just filter pkce
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "pkce" {
|
||||
continue
|
||||
}
|
||||
r.AddCookie(cookie)
|
||||
}
|
||||
r.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func NewHTTP(cfg *Config) (*HttpFrontend, error) {
|
||||
var signingKey []byte
|
||||
var err error
|
||||
if cfg.Oauth != nil {
|
||||
signingKey, err = deriveKey(cfg.Oauth.SigningKey, 32)
|
||||
signingKey, err = endpoints.DeriveKey(cfg.Oauth.SigningKey, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/openziti/zrok/endpoints"
|
||||
"github.com/openziti/zrok/endpoints/proxyUi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -59,11 +60,11 @@ func (c *githubConfigurer) configure() error {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
signingKey, err := deriveKey(c.cfg.SigningKey, 32)
|
||||
signingKey, err := endpoints.DeriveKey(c.cfg.SigningKey, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptionKey, err := deriveKey(c.cfg.EncryptionKey, 32)
|
||||
encryptionKey, err := endpoints.DeriveKey(c.cfg.EncryptionKey, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -211,7 +212,7 @@ func (c *githubConfigurer) configure() error {
|
||||
http.Handle(fmt.Sprintf("/%v/auth/callback", c.githubCfg.Name), rp.CodeExchangeHandler(login, provider))
|
||||
|
||||
logout := func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(c.cfg.CookieName)
|
||||
cookie, err := getSessionCookie(r, c.cfg.CookieName)
|
||||
if err == nil {
|
||||
tkn, err := jwt.ParseWithClaims(cookie.Value, &zrokClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
return signingKey, nil
|
||||
@@ -219,7 +220,7 @@ func (c *githubConfigurer) configure() error {
|
||||
if err == nil {
|
||||
claims := tkn.Claims.(*zrokClaims)
|
||||
if claims.Provider == c.githubCfg.Name {
|
||||
accessToken, err := decryptToken(claims.AccessToken, encryptionKey)
|
||||
accessToken, err := endpoints.DecryptToken(claims.AccessToken, encryptionKey)
|
||||
if err == nil {
|
||||
req, err := http.NewRequest("DELETE",
|
||||
fmt.Sprintf("https://api.github.com/applications/%s/token", c.githubCfg.ClientId),
|
||||
@@ -269,14 +270,7 @@ func (c *githubConfigurer) configure() error {
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: c.cfg.CookieName,
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Domain: c.cfg.CookieDomain,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
})
|
||||
clearSessionCookies(w, r, c.cfg.CookieName, c.cfg)
|
||||
|
||||
redirectURL := r.URL.Query().Get("redirect_url")
|
||||
if redirectURL == "" {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/openziti/zrok/endpoints"
|
||||
"github.com/openziti/zrok/endpoints/proxyUi"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
@@ -58,11 +59,11 @@ func (c *googleConfigurer) configure() error {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
signingKey, err := deriveKey(c.cfg.SigningKey, 32)
|
||||
signingKey, err := endpoints.DeriveKey(c.cfg.SigningKey, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptionKey, err := deriveKey(c.cfg.EncryptionKey, 32)
|
||||
encryptionKey, err := endpoints.DeriveKey(c.cfg.EncryptionKey, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -182,7 +183,7 @@ func (c *googleConfigurer) configure() error {
|
||||
http.Handle(fmt.Sprintf("/%v/auth/callback", c.googleCfg.Name), rp.CodeExchangeHandler(login, provider))
|
||||
|
||||
logout := func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(c.cfg.CookieName)
|
||||
cookie, err := getSessionCookie(r, c.cfg.CookieName)
|
||||
if err == nil {
|
||||
tkn, err := jwt.ParseWithClaims(cookie.Value, &zrokClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
return signingKey, nil
|
||||
@@ -190,7 +191,7 @@ func (c *googleConfigurer) configure() error {
|
||||
if err == nil {
|
||||
claims := tkn.Claims.(*zrokClaims)
|
||||
if claims.Provider == c.googleCfg.Name {
|
||||
accessToken, err := decryptToken(claims.AccessToken, encryptionKey)
|
||||
accessToken, err := endpoints.DecryptToken(claims.AccessToken, encryptionKey)
|
||||
if err == nil {
|
||||
revokeURL := "https://oauth2.googleapis.com/revoke"
|
||||
resp, err := http.PostForm(revokeURL, url.Values{
|
||||
@@ -231,14 +232,7 @@ func (c *googleConfigurer) configure() error {
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: c.cfg.CookieName,
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Domain: c.cfg.CookieDomain,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
})
|
||||
clearSessionCookies(w, r, c.cfg.CookieName, c.cfg)
|
||||
|
||||
redirectURL := r.URL.Query().Get("redirect_url")
|
||||
if redirectURL == "" {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/openziti/zrok/endpoints"
|
||||
"github.com/openziti/zrok/endpoints/proxyUi"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
@@ -59,11 +60,11 @@ func (c *oidcConfigurer) configure() error {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
signingKey, err := deriveKey(c.cfg.SigningKey, 32)
|
||||
signingKey, err := endpoints.DeriveKey(c.cfg.SigningKey, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encryptionKey, err := deriveKey(c.cfg.EncryptionKey, 32)
|
||||
encryptionKey, err := endpoints.DeriveKey(c.cfg.EncryptionKey, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,7 +140,7 @@ func (c *oidcConfigurer) configure() error {
|
||||
return
|
||||
}
|
||||
|
||||
cookie, err := r.Cookie(c.cfg.CookieName)
|
||||
cookie, err := getSessionCookie(r, c.cfg.CookieName)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to get auth session cookie: %v", err)
|
||||
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("unable to get auth session cookie")))
|
||||
@@ -162,7 +163,7 @@ func (c *oidcConfigurer) configure() error {
|
||||
return
|
||||
}
|
||||
|
||||
accessToken, err := decryptToken(claims.AccessToken, encryptionKey)
|
||||
accessToken, err := endpoints.DecryptToken(claims.AccessToken, encryptionKey)
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to decrypt access token: %v", err)
|
||||
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedUser(claims.Email).WithError(errors.New("unable to decrypt access token")))
|
||||
@@ -228,7 +229,7 @@ func (c *oidcConfigurer) configure() error {
|
||||
http.Handle(fmt.Sprintf("/%v/auth/callback", c.oidcCfg.Name), rp.CodeExchangeHandler(rp.UserinfoCallback(login), provider))
|
||||
|
||||
logout := func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(c.cfg.CookieName)
|
||||
cookie, err := getSessionCookie(r, c.cfg.CookieName)
|
||||
if err == nil {
|
||||
tkn, err := jwt.ParseWithClaims(cookie.Value, &zrokClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
return signingKey, nil
|
||||
@@ -236,7 +237,7 @@ func (c *oidcConfigurer) configure() error {
|
||||
if err == nil {
|
||||
claims := tkn.Claims.(*zrokClaims)
|
||||
if claims.Provider == c.oidcCfg.Name {
|
||||
accessToken, err := decryptToken(claims.AccessToken, encryptionKey)
|
||||
accessToken, err := endpoints.DecryptToken(claims.AccessToken, encryptionKey)
|
||||
if err == nil {
|
||||
if err := rp.RevokeToken(context.Background(), provider, accessToken, "access_token"); err == nil {
|
||||
logrus.Infof("revoked access token for '%v'", claims.Email)
|
||||
@@ -266,14 +267,7 @@ func (c *oidcConfigurer) configure() error {
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: c.cfg.CookieName,
|
||||
Value: "",
|
||||
MaxAge: -1,
|
||||
Domain: c.cfg.CookieDomain,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
})
|
||||
clearSessionCookies(w, r, c.cfg.CookieName, c.cfg)
|
||||
|
||||
redirectURL := r.URL.Query().Get("redirect_url")
|
||||
if redirectURL == "" {
|
||||
|
||||
Reference in New Issue
Block a user