diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md index 940c3dde..4783c63a 100644 --- a/docs/guide/authentication.md +++ b/docs/guide/authentication.md @@ -62,6 +62,34 @@ users: Dozzle uses [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token) to generate tokens for authentication. This token is saved in a cookie. +### Extending Authentication Cookie Lifetime + +By default, Dozzle uses session cookies which expire when the browser is closed. You can extend the lifetime of the cookie by setting `--auth-ttl` to a duration. Here is an example: + +::: code-group + +```sh [cli] +$ docker run -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/dozzle/data:/data -p 8080:8080 amir20/dozzle --auth-provider simple --auth-ttl 48h +``` + +```yaml [docker-compose.yml] +services: + dozzle: + image: amir20/dozzle:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /path/to/dozzle/data:/data + ports: + - 8080:8080 + environment: + DOZZLE_AUTH_PROVIDER: simple + DOZZLE_AUTH_TTL: 48h +``` + +::: + +Note that only the duration is supported. You can only use `s`, `m`, `h` for seconds, minutes and hours respectively. + ## Generating users.yml Dozzle has a builtin `generate` command to generate `users.yml`. Here is an example: diff --git a/internal/auth/simple.go b/internal/auth/simple.go index 59969bb4..31c0c9ee 100644 --- a/internal/auth/simple.go +++ b/internal/auth/simple.go @@ -12,11 +12,12 @@ import ( type simpleAuthContext struct { UserDatabase UserDatabase tokenAuth *jwtauth.JWTAuth + ttl time.Duration } var ErrInvalidCredentials = errors.New("invalid credentials") -func NewSimpleAuth(userDatabase UserDatabase) *simpleAuthContext { +func NewSimpleAuth(userDatabase UserDatabase, ttl time.Duration) *simpleAuthContext { h := sha256.New() for _, user := range userDatabase.Users { h.Write([]byte(user.Password)) @@ -27,6 +28,7 @@ func NewSimpleAuth(userDatabase UserDatabase) *simpleAuthContext { return &simpleAuthContext{ UserDatabase: userDatabase, tokenAuth: tokenAuth, + ttl: ttl, } } @@ -36,7 +38,14 @@ func (a *simpleAuthContext) CreateToken(username, password string) (string, erro return "", ErrInvalidCredentials } - _, tokenString, err := a.tokenAuth.Encode(map[string]interface{}{"username": user.Username, "email": user.Email, "name": user.Name, "timestamp": time.Now()}) + claims := map[string]interface{}{"username": user.Username, "email": user.Email, "name": user.Name} + jwtauth.SetIssuedNow(claims) + + if a.ttl > 0 { + jwtauth.SetExpiryIn(claims, a.ttl) + } + + _, tokenString, err := a.tokenAuth.Encode(claims) if err != nil { return "", err } diff --git a/internal/support/cli/args.go b/internal/support/cli/args.go index b00a13ca..870f14a5 100644 --- a/internal/support/cli/args.go +++ b/internal/support/cli/args.go @@ -15,6 +15,7 @@ type Args struct { Hostname string `arg:"env:DOZZLE_HOSTNAME" help:"sets the hostname for display. This is useful with multiple Dozzle instances."` Level string `arg:"env:DOZZLE_LEVEL" default:"info" help:"set Dozzle log level. Use debug for more logging."` AuthProvider string `arg:"--auth-provider,env:DOZZLE_AUTH_PROVIDER" default:"none" help:"sets the auth provider to use. Currently only forward-proxy is supported."` + AuthTTL string `arg:"--auth-ttl,env:DOZZLE_AUTH_TTL" default:"session" help:"sets the TTL for the auth token. Accepts duration values like 12h. Valid time units are s, m, h"` AuthHeaderUser string `arg:"--auth-header-user,env:DOZZLE_AUTH_HEADER_USER" default:"Remote-User" help:"sets the HTTP Header to use for username in Forward Proxy configuration."` AuthHeaderEmail string `arg:"--auth-header-email,env:DOZZLE_AUTH_HEADER_EMAIL" default:"Remote-Email" help:"sets the HTTP Header to use for email in Forward Proxy configuration."` AuthHeaderName string `arg:"--auth-header-name,env:DOZZLE_AUTH_HEADER_NAME" default:"Remote-Name" help:"sets the HTTP Header to use for name in Forward Proxy configuration."` diff --git a/internal/web/auth.go b/internal/web/auth.go index fbe94296..83d96def 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -12,12 +12,18 @@ func (h *handler) createToken(w http.ResponseWriter, r *http.Request) { pass := r.PostFormValue("password") if token, err := h.config.Authorization.Authorizer.CreateToken(user, pass); err == nil { + expires := time.Time{} + if h.config.Authorization.TTL > 0 { + expires = time.Now().Add(h.config.Authorization.TTL) + } + http.SetCookie(w, &http.Cookie{ Name: "jwt", Value: token, HttpOnly: true, Path: "/", SameSite: http.SameSiteLaxMode, + Expires: expires, }) log.Info().Str("user", user).Msg("Token created") w.WriteHeader(http.StatusOK) diff --git a/internal/web/auth_simple_test.go b/internal/web/auth_simple_test.go index 2833db2e..fa20a5f0 100644 --- a/internal/web/auth_simple_test.go +++ b/internal/web/auth_simple_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "strings" + "time" "testing" @@ -32,7 +33,7 @@ func Test_createRoutes_simple_redirect(t *testing.T) { Password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", }, }, - }), + }, time.Second*100), }, }) req, err := http.NewRequest("GET", "/", nil) @@ -57,7 +58,7 @@ func Test_createRoutes_simple_valid_token(t *testing.T) { Password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", }, }, - }), + }, time.Second*100), }, }) @@ -102,7 +103,7 @@ func Test_createRoutes_simple_bad_password(t *testing.T) { Password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", }, }, - }), + }, time.Second*100), }, }) diff --git a/internal/web/routes.go b/internal/web/routes.go index ec648448..b6d5c117 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -2,6 +2,7 @@ package web import ( "io/fs" + "time" "net/http" "strings" @@ -36,6 +37,7 @@ type Config struct { type Authorization struct { Provider AuthProvider Authorizer Authorizer + TTL time.Duration } type Authorizer interface { diff --git a/main.go b/main.go index b2ef50d2..2baa3894 100644 --- a/main.go +++ b/main.go @@ -229,7 +229,24 @@ func createServer(args cli.Args, multiHostService *docker_support.MultiHostServi } log.Debug().Int("users", len(db.Users)).Msg("Loaded users") - authorizer = auth.NewSimpleAuth(db) + ttl := time.Duration(0) + if args.AuthTTL != "session" { + ttl, err = time.ParseDuration(args.AuthTTL) + if err != nil { + log.Fatal().Err(err).Msg("Could not parse auth ttl") + } + } + authorizer = auth.NewSimpleAuth(db, ttl) + } + + authTTL := time.Duration(0) + + if args.AuthTTL != "session" { + ttl, err := time.ParseDuration(args.AuthTTL) + if err != nil { + log.Fatal().Err(err).Msg("Could not parse auth ttl") + } + authTTL = ttl } config := web.Config{ @@ -242,6 +259,7 @@ func createServer(args cli.Args, multiHostService *docker_support.MultiHostServi Authorization: web.Authorization{ Provider: provider, Authorizer: authorizer, + TTL: authTTL, }, EnableActions: args.EnableActions, }