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:
Livio Amstutz
2020-08-31 08:49:35 +02:00
committed by GitHub
parent 1089193faf
commit c1c85e632b
26 changed files with 262 additions and 205 deletions

View File

@@ -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,
})

View File

@@ -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) {

View File

@@ -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)
)

View 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
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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)
}
}
}

View File

@@ -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)),