mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 00:27:31 +00:00
fix: cookie handling (#654)
* feat: set cookie prefix and max age * cookie prefix on csrf cookie * fix: check user agent cookie in login * update oidc pkg * cleanup
This commit is contained in:
@@ -8,9 +8,15 @@ import (
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
prefixSecure = "__Secure-"
|
||||
prefixHost = "__Host-"
|
||||
)
|
||||
|
||||
type CookieHandler struct {
|
||||
securecookie *securecookie.SecureCookie
|
||||
secureOnly bool
|
||||
httpOnly bool
|
||||
sameSite http.SameSite
|
||||
path string
|
||||
maxAge int
|
||||
@@ -20,6 +26,7 @@ type CookieHandler struct {
|
||||
func NewCookieHandler(opts ...CookieHandlerOpt) *CookieHandler {
|
||||
c := &CookieHandler{
|
||||
secureOnly: true,
|
||||
httpOnly: true,
|
||||
sameSite: http.SameSiteLaxMode,
|
||||
path: "/",
|
||||
}
|
||||
@@ -44,6 +51,12 @@ func WithUnsecure() CookieHandlerOpt {
|
||||
}
|
||||
}
|
||||
|
||||
func WithNonHttpOnly() CookieHandlerOpt {
|
||||
return func(c *CookieHandler) {
|
||||
c.httpOnly = false
|
||||
}
|
||||
}
|
||||
|
||||
func WithSameSite(sameSite http.SameSite) CookieHandlerOpt {
|
||||
return func(c *CookieHandler) {
|
||||
c.sameSite = sameSite
|
||||
@@ -69,6 +82,16 @@ func WithDomain(domain string) CookieHandlerOpt {
|
||||
}
|
||||
}
|
||||
|
||||
func SetCookiePrefix(name, domain, path string, secureOnly bool) string {
|
||||
if !secureOnly {
|
||||
return name
|
||||
}
|
||||
if domain != "" || path != "/" {
|
||||
return prefixSecure + name
|
||||
}
|
||||
return prefixHost + name
|
||||
}
|
||||
|
||||
func (c *CookieHandler) GetCookieValue(r *http.Request, name string) (string, error) {
|
||||
cookie, err := r.Cookie(name)
|
||||
if err != nil {
|
||||
@@ -78,7 +101,7 @@ func (c *CookieHandler) GetCookieValue(r *http.Request, name string) (string, er
|
||||
}
|
||||
|
||||
func (c *CookieHandler) GetEncryptedCookieValue(r *http.Request, name string, value interface{}) error {
|
||||
cookie, err := r.Cookie(name)
|
||||
cookie, err := r.Cookie(SetCookiePrefix(name, c.domain, c.path, c.secureOnly))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -110,12 +133,12 @@ func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) {
|
||||
|
||||
func (c *CookieHandler) httpSet(w http.ResponseWriter, name, value string, maxage int) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: name,
|
||||
Name: SetCookiePrefix(name, c.domain, c.path, c.secureOnly),
|
||||
Value: value,
|
||||
Domain: c.domain,
|
||||
Path: c.path,
|
||||
MaxAge: maxage,
|
||||
HttpOnly: true,
|
||||
HttpOnly: c.httpOnly,
|
||||
Secure: c.secureOnly,
|
||||
SameSite: c.sameSite,
|
||||
})
|
||||
|
@@ -41,13 +41,13 @@ var (
|
||||
remoteAddr key
|
||||
)
|
||||
|
||||
func CopyHeadersToContext(h http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
func CopyHeadersToContext(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), httpHeaders, r.Header)
|
||||
ctx = context.WithValue(ctx, remoteAddr, r.RemoteAddr)
|
||||
r = r.WithContext(ctx)
|
||||
h(w, r)
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func HeadersFromCtx(ctx context.Context) (http.Header, bool) {
|
||||
|
@@ -9,10 +9,10 @@ import (
|
||||
http_utils "github.com/caos/zitadel/internal/api/http"
|
||||
)
|
||||
|
||||
type key int
|
||||
type securityKey int
|
||||
|
||||
const (
|
||||
nonceKey key = 0
|
||||
nonceKey securityKey = 0
|
||||
|
||||
DefaultNonceLength = uint(32)
|
||||
)
|
||||
|
103
internal/api/http/middleware/user_agent_cookie.go
Normal file
103
internal/api/http/middleware/user_agent_cookie.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
http_utils "github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
)
|
||||
|
||||
type cookieKey int
|
||||
|
||||
var (
|
||||
userAgentKey cookieKey = 0
|
||||
)
|
||||
|
||||
func UserAgentIDFromCtx(ctx context.Context) (string, bool) {
|
||||
userAgentID, ok := ctx.Value(userAgentKey).(string)
|
||||
return userAgentID, ok
|
||||
}
|
||||
|
||||
type UserAgent struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type userAgentHandler struct {
|
||||
cookieHandler *http_utils.CookieHandler
|
||||
cookieName string
|
||||
idGenerator id.Generator
|
||||
nextHandler http.Handler
|
||||
}
|
||||
|
||||
type UserAgentCookieConfig struct {
|
||||
Name string
|
||||
Domain string
|
||||
Key *crypto.KeyConfig
|
||||
MaxAge types.Duration
|
||||
}
|
||||
|
||||
func NewUserAgentHandler(config *UserAgentCookieConfig, idGenerator id.Generator, localDevMode bool) (func(http.Handler) http.Handler, error) {
|
||||
key, err := crypto.LoadKey(config.Key, config.Key.EncryptionKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cookieKey := []byte(key)
|
||||
opts := []http_utils.CookieHandlerOpt{
|
||||
http_utils.WithEncryption(cookieKey, cookieKey),
|
||||
http_utils.WithDomain(config.Domain),
|
||||
http_utils.WithMaxAge(int(config.MaxAge.Seconds())),
|
||||
}
|
||||
if localDevMode {
|
||||
opts = append(opts, http_utils.WithUnsecure())
|
||||
}
|
||||
return func(handler http.Handler) http.Handler {
|
||||
return &userAgentHandler{
|
||||
nextHandler: handler,
|
||||
cookieName: config.Name,
|
||||
cookieHandler: http_utils.NewCookieHandler(opts...),
|
||||
idGenerator: idGenerator,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ua *userAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
agent, err := ua.getUserAgent(r)
|
||||
if err != nil {
|
||||
agent, err = ua.newUserAgent()
|
||||
}
|
||||
if err == nil {
|
||||
ctx := context.WithValue(r.Context(), userAgentKey, agent.ID)
|
||||
r = r.WithContext(ctx)
|
||||
ua.setUserAgent(w, agent)
|
||||
}
|
||||
ua.nextHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (ua *userAgentHandler) newUserAgent() (*UserAgent, error) {
|
||||
agentID, err := ua.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UserAgent{ID: agentID}, nil
|
||||
}
|
||||
|
||||
func (ua *userAgentHandler) getUserAgent(r *http.Request) (*UserAgent, error) {
|
||||
userAgent := new(UserAgent)
|
||||
err := ua.cookieHandler.GetEncryptedCookieValue(r, ua.cookieName, userAgent)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowPermissionDenied(err, "HTTP-YULqH4", "cannot read user agent cookie")
|
||||
}
|
||||
return userAgent, nil
|
||||
}
|
||||
|
||||
func (ua *userAgentHandler) setUserAgent(w http.ResponseWriter, agent *UserAgent) error {
|
||||
err := ua.cookieHandler.SetEncryptedCookie(w, ua.cookieName, agent)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "HTTP-AqgqdA", "cannot set user agent cookie")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -1,68 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
)
|
||||
|
||||
type UserAgent struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type UserAgentHandler struct {
|
||||
handler *CookieHandler
|
||||
cookieName string
|
||||
idGenerator id.Generator
|
||||
}
|
||||
|
||||
type UserAgentCookieConfig struct {
|
||||
Name string
|
||||
Domain string
|
||||
Key *crypto.KeyConfig
|
||||
}
|
||||
|
||||
func NewUserAgentHandler(config *UserAgentCookieConfig, idGenerator id.Generator) (*UserAgentHandler, error) {
|
||||
key, err := crypto.LoadKey(config.Key, config.Key.EncryptionKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cookieKey := []byte(key)
|
||||
handler := NewCookieHandler(
|
||||
WithEncryption(cookieKey, cookieKey),
|
||||
WithDomain(config.Domain),
|
||||
WithUnsecure(),
|
||||
)
|
||||
return &UserAgentHandler{
|
||||
cookieName: config.Name,
|
||||
handler: handler,
|
||||
idGenerator: idGenerator,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ua *UserAgentHandler) NewUserAgent() (*UserAgent, error) {
|
||||
agentID, err := ua.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UserAgent{ID: agentID}, nil
|
||||
}
|
||||
|
||||
func (ua *UserAgentHandler) GetUserAgent(r *http.Request) (*UserAgent, error) {
|
||||
userAgent := new(UserAgent)
|
||||
err := ua.handler.GetEncryptedCookieValue(r, ua.cookieName, userAgent)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowPermissionDenied(err, "HTTP-YULqH4", "cannot read user agent cookie")
|
||||
}
|
||||
return userAgent, nil
|
||||
}
|
||||
|
||||
func (ua *UserAgentHandler) SetUserAgent(w http.ResponseWriter, agent *UserAgent) error {
|
||||
err := ua.handler.SetEncryptedCookie(w, ua.cookieName, agent)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "HTTP-AqgqdA", "cannot set user agent cookie")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -9,13 +9,13 @@ import (
|
||||
"github.com/caos/oidc/pkg/op"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
"github.com/caos/zitadel/internal/api/http/middleware"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
grant_model "github.com/caos/zitadel/internal/usergrant/model"
|
||||
)
|
||||
|
||||
func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest, userID string) (op.AuthRequest, error) {
|
||||
userAgentID, ok := UserAgentIDFromCtx(ctx)
|
||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sd436", "no user agent id")
|
||||
}
|
||||
@@ -28,7 +28,11 @@ func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest
|
||||
}
|
||||
|
||||
func (o *OPStorage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) {
|
||||
resp, err := o.repo.AuthRequestByIDCheckLoggedIn(ctx, id)
|
||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "OIDC-D3g21", "no user agent id")
|
||||
}
|
||||
resp, err := o.repo.AuthRequestByIDCheckLoggedIn(ctx, id, userAgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -44,7 +48,11 @@ func (o *OPStorage) AuthRequestByCode(ctx context.Context, code string) (op.Auth
|
||||
}
|
||||
|
||||
func (o *OPStorage) SaveAuthCode(ctx context.Context, id, code string) error {
|
||||
return o.repo.SaveAuthCode(ctx, id, code)
|
||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return errors.ThrowPreconditionFailed(nil, "OIDC-Dgus2", "no user agent id")
|
||||
}
|
||||
return o.repo.SaveAuthCode(ctx, id, code, userAgentID)
|
||||
}
|
||||
|
||||
func (o *OPStorage) DeleteAuthRequest(ctx context.Context, id string) error {
|
||||
@@ -52,16 +60,13 @@ func (o *OPStorage) DeleteAuthRequest(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
func (o *OPStorage) CreateToken(ctx context.Context, authReq op.AuthRequest) (string, time.Time, error) {
|
||||
req, err := o.repo.AuthRequestByID(ctx, authReq.GetID())
|
||||
app, err := o.repo.ApplicationByClientID(ctx, authReq.GetClientID())
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
app, err := o.repo.ApplicationByClientID(ctx, req.ApplicationID)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
grants, err := o.repo.UserGrantsByProjectAndUserID(app.ProjectID, req.UserID)
|
||||
scopes := append(req.Request.(*model.AuthRequestOIDC).Scopes, grantsToScopes(grants)...)
|
||||
grants, err := o.repo.UserGrantsByProjectAndUserID(app.ProjectID, authReq.GetSubject())
|
||||
scopes := append(authReq.GetScopes(), grantsToScopes(grants)...)
|
||||
req, _ := authReq.(*AuthRequest)
|
||||
resp, err := o.repo.CreateToken(ctx, req.AgentID, req.ApplicationID, req.UserID, req.Audience, scopes, o.defaultAccessTokenLifetime) //PLANNED: lifetime from client
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
@@ -80,7 +85,7 @@ func grantsToScopes(grants []*grant_model.UserGrantView) []string {
|
||||
}
|
||||
|
||||
func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID string) error {
|
||||
userAgentID, ok := UserAgentIDFromCtx(ctx)
|
||||
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return errors.ThrowPreconditionFailed(nil, "OIDC-fso7F", "no user agent id")
|
||||
}
|
||||
|
@@ -1,37 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
http_utils "github.com/caos/zitadel/internal/api/http"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
var (
|
||||
userAgentKey key
|
||||
)
|
||||
|
||||
func UserAgentIDFromCtx(ctx context.Context) (string, bool) {
|
||||
userAgentID, ok := ctx.Value(userAgentKey).(string)
|
||||
return userAgentID, ok
|
||||
}
|
||||
|
||||
func UserAgentCookieHandler(cookieHandler *http_utils.UserAgentHandler, nextHandlerFunc func(http.HandlerFunc) http.HandlerFunc) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(handlerFunc http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ua, err := cookieHandler.GetUserAgent(r)
|
||||
if err != nil {
|
||||
ua, err = cookieHandler.NewUserAgent()
|
||||
}
|
||||
if err == nil {
|
||||
ctx := context.WithValue(r.Context(), userAgentKey, ua.ID)
|
||||
r = r.WithContext(ctx)
|
||||
cookieHandler.SetUserAgent(w, ua)
|
||||
}
|
||||
handlerFunc(w, r)
|
||||
nextHandlerFunc(handlerFunc)
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,7 +2,6 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
@@ -18,7 +17,7 @@ import (
|
||||
type OPHandlerConfig struct {
|
||||
OPConfig *op.Config
|
||||
StorageConfig StorageConfig
|
||||
UserAgentCookieConfig *http_utils.UserAgentCookieConfig
|
||||
UserAgentCookieConfig *middleware.UserAgentCookieConfig
|
||||
Cache *middleware.CacheConfig
|
||||
Endpoints *EndpointConfig
|
||||
}
|
||||
@@ -51,24 +50,18 @@ type OPStorage struct {
|
||||
signingKeyAlgorithm string
|
||||
}
|
||||
|
||||
func NewProvider(ctx context.Context, config OPHandlerConfig, repo repository.Repository) op.OpenIDProvider {
|
||||
cookieHandler, err := http_utils.NewUserAgentHandler(config.UserAgentCookieConfig, id.SonyFlakeGenerator)
|
||||
func NewProvider(ctx context.Context, config OPHandlerConfig, repo repository.Repository, localDevMode bool) op.OpenIDProvider {
|
||||
cookieHandler, err := middleware.NewUserAgentHandler(config.UserAgentCookieConfig, id.SonyFlakeGenerator, localDevMode)
|
||||
logging.Log("OIDC-sd4fd").OnError(err).Panic("cannot user agent handler")
|
||||
nextHandler := func(handlerFunc http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.NoCacheInterceptor(http_utils.CopyHeadersToContext(handlerFunc))
|
||||
}
|
||||
}
|
||||
config.OPConfig.CodeMethodS256 = true
|
||||
provider, err := op.NewDefaultOP(
|
||||
ctx,
|
||||
config.OPConfig,
|
||||
newStorage(config.StorageConfig, repo),
|
||||
op.WithHttpInterceptor(
|
||||
UserAgentCookieHandler(
|
||||
cookieHandler,
|
||||
nextHandler,
|
||||
),
|
||||
op.WithHttpInterceptors(
|
||||
middleware.NoCacheInterceptor,
|
||||
cookieHandler,
|
||||
http_utils.CopyHeadersToContext,
|
||||
),
|
||||
op.WithCustomAuthEndpoint(op.NewEndpointWithURL(config.Endpoints.Auth.Path, config.Endpoints.Auth.URL)),
|
||||
op.WithCustomTokenEndpoint(op.NewEndpointWithURL(config.Endpoints.Token.Path, config.Endpoints.Token.URL)),
|
||||
|
Reference in New Issue
Block a user