Repository: jkroepke/openvpn-auth-oauth2
Pull request #86 (2023-12-16 21:58:23)

Switch to goflag
#### What this PR does / why we need it

#### Which issue this PR fixes
*(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #

- fixes #
 
#### Special notes for your reviewer

#### Particularly user-facing changes

#### Checklist

Complete these before marking the PR as `ready to review`:

<!-- [Place an '[x]' (no spaces) in all applicable fields.] -->
- [ ] [DCO](https://github.com/prometheus-community/helm-charts/blob/main/CONTRIBUTING.md#sign-off-your-work) signed
- [ ] The PR title has a summary of the changes
- [ ] The PR body has a summary to reflect any significant (and particularly user-facing) changes introduced by this PR


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) Report
Attention: `25 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`328b1a9`)](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/commit/328b1a93891386a4dc8d61eb1074dda2a153b609?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) 77.25% compared to head [(`ebc2815`)](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) 76.32%.
> Report is 1 commits behind head on main.

> :exclamation: Current head ebc2815 differs from pull request most recent head 72fad87. Consider uploading reports for the commit 72fad87 to get more accurate results

| [Files](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) | Patch % | Lines |
|---|---|---|
| [internal/config/secret.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvY29uZmlnL3NlY3JldC5nbw==) | 23.52% | [13 Missing :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |
| [internal/config/config.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvY29uZmlnL2NvbmZpZy5nbw==) | 64.70% | [5 Missing and 1 partial :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |
| [cmd/root.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-Y21kL3Jvb3QuZ28=) | 50.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |
| [internal/config/load.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvY29uZmlnL2xvYWQuZ28=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |
| [internal/oauth2/handler.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvb2F1dGgyL2hhbmRsZXIuZ28=) | 50.00% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |
| [internal/openvpn/handler.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvb3BlbnZwbi9oYW5kbGVyLmdv) | 0.00% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #86      +/-   ##
==========================================
- Coverage   77.25%   76.32%   -0.93%     
==========================================
  Files          22       24       +2     
  Lines        1433     1453      +20     
==========================================
+ Hits         1107     1109       +2     
- Misses        270      286      +16     
- Partials       56       58       +2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/86?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke).


Patch:
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 3f8e7d4..23a09b4 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -6,8 +6,8 @@ We as members, contributors, and leaders pledge to make participation in our
 community a harassment-free experience for everyone, regardless of age, body
 size, visible or invisible disability, ethnicity, sex characteristics, gender
 identity and expression, level of experience, education, socio-economic status,
-nationality, personal appearance, race, religion, or sexual identity
-and orientation.
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
 
 We pledge to act and interact in ways that contribute to an open, welcoming,
 diverse, inclusive, and healthy community.
@@ -22,17 +22,17 @@ community include:
 * Giving and gracefully accepting constructive feedback
 * Accepting responsibility and apologizing to those affected by our mistakes,
   and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the
-  overall community
+* Focusing on what is best not just for us as individuals, but for the overall
+  community
 
 Examples of unacceptable behavior include:
 
-* The use of sexualized language or imagery, and sexual attention or
-  advances of any kind
+* The use of sexualized language or imagery, and sexual attention or advances of
+  any kind
 * Trolling, insulting or derogatory comments, and personal or political attacks
 * Public or private harassment
-* Publishing others' private information, such as a physical or email
-  address, without their explicit permission
+* Publishing others' private information, such as a physical or email address,
+  without their explicit permission
 * Other conduct which could reasonably be considered inappropriate in a
   professional setting
 
@@ -52,7 +52,7 @@ decisions when appropriate.
 
 This Code of Conduct applies within all community spaces, and also applies when
 an individual is officially representing the community in public spaces.
-Examples of representing our community include using an official e-mail address,
+Examples of representing our community include using an official email address,
 posting via an official social media account, or acting as an appointed
 representative at an online or offline event.
 
@@ -82,15 +82,15 @@ behavior was inappropriate. A public apology may be requested.
 
 ### 2. Warning
 
-**Community Impact**: A violation through a single incident or series
-of actions.
+**Community Impact**: A violation through a single incident or series of
+actions.
 
 **Consequence**: A warning with consequences for continued behavior. No
 interaction with the people involved, including unsolicited interaction with
 those enforcing the Code of Conduct, for a specified period of time. This
 includes avoiding interactions in community spaces as well as external channels
-like social media. Violating these terms may lead to a temporary or
-permanent ban.
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
 
 ### 3. Temporary Ban
 
@@ -106,23 +106,27 @@ Violating these terms may lead to a permanent ban.
 ### 4. Permanent Ban
 
 **Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior,  harassment of an
+standards, including sustained inappropriate behavior, harassment of an
 individual, or aggression toward or disparagement of classes of individuals.
 
-**Consequence**: A permanent ban from any sort of public interaction within
-the community.
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
 
 ## Attribution
 
 This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 2.0, available at
-https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
 
-Community Impact Guidelines were inspired by [Mozilla's code of conduct
-enforcement ladder](https://github.com/mozilla/diversity).
-
-[homepage]: https://www.contributor-covenant.org
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
 
 For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq. Translations are available at
-https://www.contributor-covenant.org/translations.
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/cmd/root.go b/cmd/root.go
index e2feea0..dd5ed67 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,6 +1,8 @@
 package cmd
 
 import (
+	"errors"
+	"flag"
 	"fmt"
 	"io"
 	"log/slog"
@@ -16,29 +18,32 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
 )
 
+//nolint:cyclop
 func Execute(args []string, logWriter io.Writer, version, commit, date string) int {
 	var err error
 
-	logger := slog.New(slog.NewTextHandler(logWriter, &slog.HandlerOptions{
-		AddSource: false,
-	}))
+	logger := defaultLogger(logWriter)
+
+	flagSet := config.FlagSet(args[0])
+	flagSet.SetOutput(logWriter)
 
-	flagSet := config.FlagSet()
 	if err = flagSet.Parse(args[1:]); err != nil {
+		if errors.Is(err, flag.ErrHelp) {
+			return 0
+		}
+
 		logger.Error(fmt.Errorf("error parsing cli args: %w", err).Error())
 
 		return 1
 	}
 
-	if versionFlag, _ := flagSet.GetBool("version"); versionFlag {
-		fmt.Printf("version: %s\ncommit: %s\ndate: %s\ngo: %s\n", version, commit, date, runtime.Version())
+	if flagSet.Lookup("version").Value.String() == "true" {
+		fmt.Fprintf(logWriter, "version: %s\ncommit: %s\ndate: %s\ngo: %s\n", version, commit, date, runtime.Version())
 
 		return 0
 	}
 
-	configFile, _ := flagSet.GetString("config")
-
-	conf, err := config.Load(config.ManagementClient, configFile, flagSet)
+	conf, err := config.Load(config.ManagementClient, flagSet.Lookup("config").Value.String(), flagSet)
 	if err != nil {
 		logger.Error(fmt.Errorf("error loading config: %w", err).Error())
 
@@ -47,9 +52,7 @@ func Execute(args []string, logWriter io.Writer, version, commit, date string) i
 
 	logger, err = configureLogger(conf, logWriter)
 	if err != nil {
-		logger := slog.New(slog.NewTextHandler(logWriter, &slog.HandlerOptions{
-			AddSource: false,
-		}))
+		logger = defaultLogger(logWriter)
 		logger.Error(fmt.Errorf("error configure logging: %w", err).Error())
 
 		return 1
@@ -91,7 +94,7 @@ func Execute(args []string, logWriter io.Writer, version, commit, date string) i
 	}()
 
 	termCh := make(chan os.Signal, 1)
-	signal.Notify(termCh, syscall.SIGINT, syscall.SIGTERM)
+	signal.Notify(termCh, os.Interrupt, syscall.SIGTERM)
 
 	var returnCode int
 	select {
@@ -119,17 +122,16 @@ func shutdown(logger *slog.Logger, openvpnClient *openvpn.Client, server http.Se
 	logger.Info("http listener successfully terminated")
 }
 
-func configureLogger(conf config.Config, writer io.Writer) (*slog.Logger, error) {
-	var level slog.Level
-
-	err := level.UnmarshalText([]byte(conf.Log.Level))
-	if err != nil {
-		return nil, fmt.Errorf("unable to parse log level: %w", err)
-	}
+func defaultLogger(writer io.Writer) *slog.Logger {
+	return slog.New(slog.NewTextHandler(writer, &slog.HandlerOptions{
+		AddSource: false,
+	}))
+}
 
+func configureLogger(conf config.Config, writer io.Writer) (*slog.Logger, error) {
 	opts := &slog.HandlerOptions{
 		AddSource: false,
-		Level:     level,
+		Level:     conf.Log.Level,
 	}
 
 	switch conf.Log.Format {
diff --git a/go.mod b/go.mod
index b5460a4..8bb045c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,14 +4,13 @@ go 1.21
 
 require (
 	github.com/knadh/koanf/parsers/yaml v0.1.0
+	github.com/knadh/koanf/providers/basicflag v0.1.0
 	github.com/knadh/koanf/providers/env v0.1.0
 	github.com/knadh/koanf/providers/file v0.1.0
-	github.com/knadh/koanf/providers/posflag v0.1.0
 	github.com/knadh/koanf/providers/structs v0.1.0
 	github.com/knadh/koanf/v2 v2.0.1
 	github.com/madflojo/testcerts v1.1.1
 	github.com/mitchellh/mapstructure v1.5.0
-	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.8.4
 	github.com/zitadel/oidc/v3 v3.6.0
 	golang.org/x/oauth2 v0.15.0
@@ -27,7 +26,7 @@ require (
 	github.com/go-logr/logr v1.3.0 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
-	github.com/google/uuid v1.4.0 // indirect
+	github.com/google/uuid v1.5.0 // indirect
 	github.com/gorilla/securecookie v1.1.2 // indirect
 	github.com/knadh/koanf/maps v0.1.1 // indirect
 	github.com/kr/text v0.2.0 // indirect
@@ -44,9 +43,14 @@ require (
 	go.opentelemetry.io/otel/metric v1.21.0 // indirect
 	go.opentelemetry.io/otel/trace v1.21.0 // indirect
 	golang.org/x/crypto v0.16.0 // indirect
-	golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
+	golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
 	golang.org/x/sys v0.15.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
 	google.golang.org/protobuf v1.31.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
+
+replace (
+	github.com/knadh/koanf/providers/basicflag v0.1.0 => github.com/knadh/koanf/providers/basicflag v0.0.0-20231215124341-b92d5e403ef3
+	github.com/mitchellh/mapstructure v1.5.0 => github.com/mitchellh/mapstructure v0.0.0-20231216201459-8508981c8b6c
+)
diff --git a/go.sum b/go.sum
index ec11a29..05423ac 100644
--- a/go.sum
+++ b/go.sum
@@ -27,8 +27,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
 github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
 github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
@@ -37,12 +37,12 @@ github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NI
 github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
 github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
 github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
+github.com/knadh/koanf/providers/basicflag v0.0.0-20231215124341-b92d5e403ef3 h1:ZS1Ka7vERnp8mQshm5uSnO9vtlAzBvZTvhIr8qL9nQw=
+github.com/knadh/koanf/providers/basicflag v0.0.0-20231215124341-b92d5e403ef3/go.mod h1:n0NlnaxXUCER/WIzRroT9q3Np+FiZ9pSjrC6A/OozI8=
 github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
 github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
 github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
 github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
-github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U=
-github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
 github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0=
 github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE=
 github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
@@ -55,8 +55,8 @@ github.com/madflojo/testcerts v1.1.1 h1:YsSHWV79nMNZK0mJtwXjKoYHjJEbLPFefR8TxmmW
 github.com/madflojo/testcerts v1.1.1/go.mod h1:MW8sh39gLnkKh4K0Nc55AyHEDl9l/FBLDUsQhpmkuo0=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v0.0.0-20231216201459-8508981c8b6c h1:7yALzQfWNpbcw8+dASu8Dz6T6cL+/DBz8SurtF8EgoY=
+github.com/mitchellh/mapstructure v0.0.0-20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
@@ -69,8 +69,6 @@ github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
 github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -94,8 +92,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
 golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
-golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
+golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
+golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
diff --git a/go.work.sum b/go.work.sum
index e2f72ed..b4c1483 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -27,7 +27,10 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
 github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
+github.com/knadh/koanf/providers/basicflag v0.1.0 h1:NZwVblBNHBUrjzk+rqY1TlBC6ariah0lj4iotjZRUYs=
+github.com/knadh/koanf/providers/basicflag v0.1.0/go.mod h1:n0NlnaxXUCER/WIzRroT9q3Np+FiZ9pSjrC6A/OozI8=
 github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
+github.com/mitchellh/mapstructure v0.0.0-20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
diff --git a/internal/config/config.go b/internal/config/config.go
index 949e6f9..32819f6 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -2,13 +2,12 @@ package config
 
 import (
 	"errors"
+	"flag"
 	"fmt"
+	"log/slog"
 	"net/url"
-	"os"
 	"slices"
 	"strings"
-
-	flag "github.com/spf13/pflag"
 )
 
 const (
@@ -17,25 +16,28 @@ const (
 )
 
 // FlagSet configure the command line parser using the [flag] library.
-func FlagSet() *flag.FlagSet {
-	flagSet := flag.NewFlagSet("openvpn-auth-oauth2", flag.ContinueOnError)
+func FlagSet(name string) *flag.FlagSet {
+	flagSet := flag.NewFlagSet(name, flag.ContinueOnError)
 	flagSet.Usage = func() {
-		fmt.Println("Usage of openvpn-auth-oauth2:")
+		fmt.Fprintf(flagSet.Output(), "Usage of %s:\n\n", name)
+		// --help should display options with double dash
+		flagSet.VisitAll(func(flag *flag.Flag) {
+			flag.Name = "-" + flag.Name
+		})
 		flagSet.PrintDefaults()
-		os.Exit(0)
 	}
 
 	flagSet.String(
 		"config",
 		"",
-		"path to one .yaml config files",
+		"path to one .yaml config file",
 	)
 	flagSet.String(
 		"log.format",
 		Defaults.Log.Format,
 		"log format. json or console",
 	)
-	flagSet.String(
+	flagSet.TextVar(new(slog.Level),
 		"log.level",
 		Defaults.Log.Level,
 		"log level",
@@ -55,7 +57,7 @@ func FlagSet() *flag.FlagSet {
 		Defaults.HTTP.BaseURL.String(),
 		"listen addr for client listener",
 	)
-	flagSet.String(
+	flagSet.TextVar(new(Secret),
 		"http.secret",
 		Defaults.HTTP.Secret,
 		"Cookie secret",
@@ -90,7 +92,7 @@ func FlagSet() *flag.FlagSet {
 		Defaults.OpenVpn.Addr.String(),
 		"openvpn management interface addr. Must start with unix:// or tcp://",
 	)
-	flagSet.String(
+	flagSet.TextVar(new(Secret),
 		"openvpn.password",
 		Defaults.OpenVpn.Password,
 		"openvpn management interface password",
@@ -105,7 +107,7 @@ func FlagSet() *flag.FlagSet {
 		Defaults.OpenVpn.AuthPendingTimeout.String(),
 		"How long OpenVPN server wait until user is authenticated",
 	)
-	flagSet.StringSlice(
+	flagSet.TextVar(new(StringSlice),
 		"openvpn.bypass.cn",
 		Defaults.OpenVpn.Bypass.CommonNames,
 		"bypass oauth authentication for CNs",
@@ -151,17 +153,17 @@ func FlagSet() *flag.FlagSet {
 		Defaults.OAuth2.Client.ID,
 		"oauth2 client id",
 	)
-	flagSet.String(
+	flagSet.TextVar(new(Secret),
 		"oauth2.client.secret",
 		Defaults.OAuth2.Client.Secret,
 		"oauth2 client secret",
 	)
-	flagSet.StringSlice(
+	flagSet.TextVar(new(StringSlice),
 		"oauth2.validate.groups",
 		Defaults.OAuth2.Validate.Groups,
 		"oauth2 required user groups",
 	)
-	flagSet.StringSlice(
+	flagSet.TextVar(new(StringSlice),
 		"oauth2.validate.roles",
 		Defaults.OAuth2.Validate.Roles,
 		"oauth2 required user roles",
@@ -181,7 +183,7 @@ func FlagSet() *flag.FlagSet {
 		Defaults.OAuth2.Validate.CommonName,
 		"validate common_name from OpenVPN with IDToken claim",
 	)
-	flagSet.StringSlice(
+	flagSet.TextVar(new(StringSlice),
 		"oauth2.scopes",
 		Defaults.OAuth2.Scopes,
 		"oauth2 token scopes. Defaults depends on oauth2.provider",
@@ -210,7 +212,6 @@ func FlagSet() *flag.FlagSet {
 // Validate validates the config.
 func Validate(mode int, conf Config) error { //nolint:cyclop
 	for key, value := range map[string]string{
-		"http.secret":      conf.HTTP.Secret,
 		"oauth2.client.id": conf.OAuth2.Client.ID,
 	} {
 		if value == "" {
@@ -218,6 +219,14 @@ func Validate(mode int, conf Config) error { //nolint:cyclop
 		}
 	}
 
+	for key, value := range map[string]Secret{
+		"http.secret": conf.HTTP.Secret,
+	} {
+		if value.String() == "" {
+			return fmt.Errorf("%s is %w", key, ErrRequired)
+		}
+	}
+
 	for key, value := range map[string]*url.URL{
 		"http.baseurl":  conf.HTTP.BaseURL,
 		"oauth2.issuer": conf.OAuth2.Issuer,
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index b18983e..da893aa 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -1,6 +1,7 @@
 package config
 
 import (
+	"log/slog"
 	"net/url"
 	"time"
 )
@@ -9,7 +10,7 @@ import (
 var Defaults = Config{
 	Log: Log{
 		Format: "console",
-		Level:  "info",
+		Level:  slog.LevelInfo,
 	},
 	HTTP: HTTP{
 		BaseURL: &url.URL{
diff --git a/internal/config/load.go b/internal/config/load.go
index 2420805..72f239d 100644
--- a/internal/config/load.go
+++ b/internal/config/load.go
@@ -1,17 +1,17 @@
 package config
 
 import (
+	"flag"
 	"fmt"
 	"strings"
 
 	"github.com/knadh/koanf/parsers/yaml"
+	"github.com/knadh/koanf/providers/basicflag"
 	"github.com/knadh/koanf/providers/env"
 	"github.com/knadh/koanf/providers/file"
-	"github.com/knadh/koanf/providers/posflag"
 	"github.com/knadh/koanf/providers/structs"
 	"github.com/knadh/koanf/v2"
 	"github.com/mitchellh/mapstructure"
-	flag "github.com/spf13/pflag"
 )
 
 const envPrefix = "CONFIG_"
@@ -32,7 +32,7 @@ func Load(mode int, configFile string, flagSet *flag.FlagSet) (Config, error) {
 	}
 
 	if flagSet != nil {
-		if err = k.Load(posflag.Provider(flagSet, ".", k), nil); err != nil {
+		if err = k.Load(basicflag.Provider(flagSet, ".", &basicflag.Opt{KeyMap: k}), nil); err != nil {
 			return Config{}, fmt.Errorf("posflag provider: %w", err)
 		}
 	}
@@ -43,11 +43,6 @@ func Load(mode int, configFile string, flagSet *flag.FlagSet) (Config, error) {
 			key = strings.ReplaceAll(key, "__", "-")
 			key = strings.ReplaceAll(key, "_", ".")
 
-			// If there is a space in the value, split the value into a slice by the space.
-			if strings.Contains(envValue, " ") {
-				return key, strings.Split(envValue, " ")
-			}
-
 			// Otherwise, return the plain string.
 			return key, envValue
 		}), nil,
diff --git a/internal/config/secret.go b/internal/config/secret.go
new file mode 100644
index 0000000..e699bd2
--- /dev/null
+++ b/internal/config/secret.go
@@ -0,0 +1,46 @@
+package config
+
+import (
+	"fmt"
+	"os"
+	"strings"
+)
+
+type Secret string
+
+// String reassembles the Secret into a valid string.
+//
+//goland:noinspection GoMixedReceiverTypes
+func (secret Secret) String() string {
+	return string(secret)
+}
+
+// MarshalText implements [encoding.TextMarshaler] interface for Secret
+//
+//goland:noinspection GoMixedReceiverTypes
+func (secret Secret) MarshalText() ([]byte, error) {
+	return []byte(secret), nil
+}
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface for Secret
+//
+//goland:noinspection GoMixedReceiverTypes
+func (secret *Secret) UnmarshalText(text []byte) error {
+	stringText := string(text)
+	if !strings.HasPrefix(stringText, "file://") {
+		*secret = Secret(stringText)
+
+		return nil
+	}
+
+	file := strings.TrimPrefix(stringText, "file://")
+
+	body, err := os.ReadFile(file)
+	if err != nil {
+		return fmt.Errorf("unable read secret: %w", err)
+	}
+
+	*secret = Secret(body)
+
+	return nil
+}
diff --git a/internal/config/slice.go b/internal/config/slice.go
new file mode 100644
index 0000000..04ac6e5
--- /dev/null
+++ b/internal/config/slice.go
@@ -0,0 +1,23 @@
+package config
+
+import (
+	"strings"
+)
+
+type StringSlice []string
+
+// MarshalText implements [encoding.TextMarshaler] interface for StringSlice
+//
+//goland:noinspection GoMixedReceiverTypes
+func (stringSlice StringSlice) MarshalText() ([]byte, error) {
+	return []byte(strings.Join(stringSlice, ",")), nil
+}
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface for StringSlice
+//
+//goland:noinspection GoMixedReceiverTypes
+func (stringSlice *StringSlice) UnmarshalText(text []byte) error {
+	*stringSlice = strings.Split(string(text), ",")
+
+	return nil
+}
diff --git a/internal/config/types.go b/internal/config/types.go
index adf72b0..ae40990 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -2,6 +2,7 @@ package config
 
 import (
 	"fmt"
+	"log/slog"
 	"net/url"
 	"strings"
 	"text/template"
@@ -22,7 +23,7 @@ type HTTP struct {
 	KeyFile            string             `koanf:"key"`
 	TLS                bool               `koanf:"tls"`
 	BaseURL            *url.URL           `koanf:"baseurl"`
-	Secret             string             `koanf:"secret"`
+	Secret             Secret             `koanf:"secret"`
 	CallbackTemplate   *template.Template `koanf:"template"`
 	Check              HTTPCheck          `koanf:"check"`
 	EnableProxyHeaders bool               `koanf:"enable-proxy-headers"`
@@ -33,13 +34,13 @@ type HTTPCheck struct {
 }
 
 type Log struct {
-	Format string `koanf:"format"`
-	Level  string `koanf:"level"`
+	Format string     `koanf:"format"`
+	Level  slog.Level `koanf:"level"`
 }
 
 type OpenVpn struct {
 	Addr               *url.URL          `koanf:"addr"`
-	Password           string            `koanf:"password"`
+	Password           Secret            `koanf:"password"`
 	Bypass             OpenVpnBypass     `koanf:"bypass"`
 	AuthTokenUser      bool              `koanf:"auth-token-user"`
 	AuthPendingTimeout time.Duration     `koanf:"auth-pending-timeout"`
@@ -47,7 +48,7 @@ type OpenVpn struct {
 }
 
 type OpenVpnBypass struct {
-	CommonNames []string `koanf:"cn"`
+	CommonNames StringSlice `koanf:"cn"`
 }
 
 type OpenVPNCommonName struct {
@@ -60,14 +61,14 @@ type OAuth2 struct {
 	AuthorizeParams string          `koanf:"authorize-params"`
 	Endpoints       OAuth2Endpoints `koanf:"endpoint"`
 	Client          OAuth2Client    `koanf:"client"`
-	Scopes          []string        `koanf:"scopes"`
+	Scopes          StringSlice     `koanf:"scopes"`
 	Pkce            bool            `koanf:"pkce"`
 	Validate        OAuth2Validate  `koanf:"validate"`
 }
 
 type OAuth2Client struct {
 	ID     string `koanf:"id"`
-	Secret string `koanf:"secret"`
+	Secret Secret `koanf:"secret"`
 }
 
 type OAuth2Endpoints struct {
@@ -77,11 +78,11 @@ type OAuth2Endpoints struct {
 }
 
 type OAuth2Validate struct {
-	Groups     []string `koanf:"groups"`
-	Roles      []string `koanf:"roles"`
-	IPAddr     bool     `koanf:"ipaddr"`
-	Issuer     bool     `koanf:"issuer"`
-	CommonName string   `koanf:"common_name"`
+	Groups     StringSlice `koanf:"groups"`
+	Roles      StringSlice `koanf:"roles"`
+	IPAddr     bool        `koanf:"ipaddr"`
+	Issuer     bool        `koanf:"issuer"`
+	CommonName string      `koanf:"common_name"`
 }
 
 type OpenVPNCommonNameMode int
diff --git a/internal/http/main.go b/internal/http/main.go
index 24fc2c3..10077be 100644
--- a/internal/http/main.go
+++ b/internal/http/main.go
@@ -9,7 +9,6 @@ import (
 	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
-	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
 )
 
 type Server struct {
@@ -41,14 +40,14 @@ func (s Server) Listen() error {
 	var err error
 
 	if s.conf.HTTP.TLS {
-		s.logger.Info(utils.StringConcat(
-			"start HTTPS server listener on ", s.conf.HTTP.Listen, " with base url ", s.conf.HTTP.BaseURL.String(),
+		s.logger.Info(fmt.Sprintf(
+			"start HTTPS server listener on %s with base url %s", s.conf.HTTP.Listen, s.conf.HTTP.BaseURL.String(),
 		))
 
 		err = s.server.ListenAndServeTLS(s.conf.HTTP.CertFile, s.conf.HTTP.KeyFile)
 	} else {
-		s.logger.Info(utils.StringConcat(
-			"start HTTP server listener on ", s.conf.HTTP.Listen, " with base url ", s.conf.HTTP.BaseURL.String(),
+		s.logger.Info(fmt.Sprintf(
+			"start HTTP server listener on %s with base url %s", s.conf.HTTP.Listen, s.conf.HTTP.BaseURL.String(),
 		))
 
 		err = s.server.ListenAndServe()
diff --git a/internal/oauth2/handler.go b/internal/oauth2/handler.go
index 61de717..739131f 100644
--- a/internal/oauth2/handler.go
+++ b/internal/oauth2/handler.go
@@ -47,7 +47,7 @@ func oauth2Start(logger *slog.Logger, provider Provider, conf config.Config, ope
 		}
 
 		session := state.NewEncoded(sessionState)
-		if err := session.Decode(conf.HTTP.Secret); err != nil {
+		if err := session.Decode(conf.HTTP.Secret.String()); err != nil {
 			logger.Warn(utils.StringConcat("invalid state: ", err.Error()))
 			logger.Debug(sessionState)
 			w.WriteHeader(http.StatusBadRequest)
@@ -135,7 +135,7 @@ func oauth2Callback(
 		}
 
 		session := state.NewEncoded(encryptedSession)
-		if err := session.Decode(conf.HTTP.Secret); err != nil {
+		if err := session.Decode(conf.HTTP.Secret.String()); err != nil {
 			logger.Warn(err.Error())
 			logger.Debug(encryptedSession)
 			writeError(w, logger, conf, http.StatusInternalServerError, "invalidSession", err.Error())
diff --git a/internal/oauth2/provider.go b/internal/oauth2/provider.go
index 68a5da2..141b9ba 100644
--- a/internal/oauth2/provider.go
+++ b/internal/oauth2/provider.go
@@ -102,7 +102,7 @@ func newProviderWithEndpoints(
 ) (Provider, error) {
 	rpConfig := &oauth2.Config{
 		ClientID:     conf.OAuth2.Client.ID,
-		ClientSecret: conf.OAuth2.Client.Secret,
+		ClientSecret: conf.OAuth2.Client.Secret.String(),
 		RedirectURL:  redirectURI,
 		Scopes:       scopes,
 		Endpoint:     endpoints,
@@ -127,7 +127,7 @@ func newProviderWithDiscovery(
 		context.Background(),
 		conf.OAuth2.Issuer.String(),
 		conf.OAuth2.Client.ID,
-		conf.OAuth2.Client.Secret,
+		conf.OAuth2.Client.Secret.String(),
 		redirectURI,
 		scopes,
 		options...,
diff --git a/internal/openvpn/client.go b/internal/openvpn/client.go
index 586b1f5..3e9d385 100644
--- a/internal/openvpn/client.go
+++ b/internal/openvpn/client.go
@@ -55,7 +55,7 @@ func (c *Client) clientConnect(client connection.Client) error {
 	commonName := utils.TransformCommonName(c.conf.OpenVpn.CommonName.Mode, client.CommonName)
 
 	session := state.New(ClientIdentifier, client.IPAddr, commonName)
-	if err = session.Encode(c.conf.HTTP.Secret); err != nil {
+	if err = session.Encode(c.conf.HTTP.Secret.String()); err != nil {
 		return fmt.Errorf("error encoding state: %w", err)
 	}
 
diff --git a/internal/openvpn/handler.go b/internal/openvpn/handler.go
index 19da64c..2316dd2 100644
--- a/internal/openvpn/handler.go
+++ b/internal/openvpn/handler.go
@@ -37,7 +37,7 @@ func (c *Client) handlePassword() error {
 
 // sendPassword enters the password on the OpenVPN management interface connection.
 func (c *Client) sendPassword() error {
-	if err := c.rawCommand(utils.StringConcat(c.conf.OpenVpn.Password, "\n")); err != nil {
+	if err := c.rawCommand(utils.StringConcat(c.conf.OpenVpn.Password.String(), "\n")); err != nil {
 		return fmt.Errorf("error from password command: %w", err)
 	}
 
diff --git a/pkg/plugin/go.mod b/pkg/plugin/go.mod
index e75df35..cb5a65d 100644
--- a/pkg/plugin/go.mod
+++ b/pkg/plugin/go.mod
@@ -2,9 +2,9 @@ module github.com/jkroepke/openvpn-auth-oauth2/pkg/plugin
 
 go 1.21
 
-require github.com/jkroepke/openvpn-auth-oauth2 v0.0.0
+require github.com/jkroepke/openvpn-auth-oauth2 v0.0.0-00000000000000-000000000000
 
-replace github.com/jkroepke/openvpn-auth-oauth2 v0.0.0 => ../../
+replace github.com/jkroepke/openvpn-auth-oauth2 v0.0.0-00000000000000-000000000000 => ../../
 
 require (
 	github.com/fatih/structs v1.1.0 // indirect
@@ -12,13 +12,13 @@ require (
 	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
-	github.com/google/uuid v1.4.0 // indirect
+	github.com/google/uuid v1.5.0 // indirect
 	github.com/gorilla/securecookie v1.1.2 // indirect
 	github.com/knadh/koanf/maps v0.1.1 // indirect
 	github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect
+	github.com/knadh/koanf/providers/basicflag v0.1.0 // indirect
 	github.com/knadh/koanf/providers/env v0.1.0 // indirect
 	github.com/knadh/koanf/providers/file v0.1.0 // indirect
-	github.com/knadh/koanf/providers/posflag v0.1.0 // indirect
 	github.com/knadh/koanf/providers/structs v0.1.0 // indirect
 	github.com/knadh/koanf/v2 v2.0.1 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
@@ -26,12 +26,11 @@ require (
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/muhlemmer/gu v0.3.1 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/zitadel/logging v0.5.0 // indirect
-	github.com/zitadel/oidc/v3 v3.4.0 // indirect
+	github.com/zitadel/oidc/v3 v3.6.0 // indirect
 	github.com/zitadel/schema v1.3.0 // indirect
 	golang.org/x/crypto v0.16.0 // indirect
-	golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
+	golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 // indirect
 	golang.org/x/oauth2 v0.15.0 // indirect
 	golang.org/x/sys v0.15.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
diff --git a/pkg/plugin/go.sum b/pkg/plugin/go.sum
index d848373..4fd46b1 100644
--- a/pkg/plugin/go.sum
+++ b/pkg/plugin/go.sum
@@ -23,8 +23,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
-github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
 github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
 github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc=
@@ -33,12 +33,12 @@ github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NI
 github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
 github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
 github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
+github.com/knadh/koanf/providers/basicflag v0.1.0 h1:NZwVblBNHBUrjzk+rqY1TlBC6ariah0lj4iotjZRUYs=
+github.com/knadh/koanf/providers/basicflag v0.1.0/go.mod h1:n0NlnaxXUCER/WIzRroT9q3Np+FiZ9pSjrC6A/OozI8=
 github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
 github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
 github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
 github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
-github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U=
-github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0=
 github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0=
 github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE=
 github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g=
@@ -67,8 +67,6 @@ github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
 github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -77,8 +75,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/zitadel/logging v0.5.0 h1:Kunouvqse/efXy4UDvFw5s3vP+Z4AlHo3y8wF7stXHA=
 github.com/zitadel/logging v0.5.0/go.mod h1:IzP5fzwFhzzyxHkSmfF8dsyqFsQRJLLcQmwhIBzlGsE=
-github.com/zitadel/oidc/v3 v3.4.0 h1:JkbNnrk/7IG+NOBoZp/P0kx6tPcBvnCekSqDTPCOok4=
-github.com/zitadel/oidc/v3 v3.4.0/go.mod h1:jUnLnx5ihKlo88cSEduZkKlzeMrjzcWVZ8fTzKBxZKY=
+github.com/zitadel/oidc/v3 v3.6.0 h1:q+yJ0Z8e2QIhtnauIp/Na0t4NavKPG50S0jgxk8Ks8g=
+github.com/zitadel/oidc/v3 v3.6.0/go.mod h1:R8sF5DPR98QQnOoyySsaNqI4NcF/VFMkf/XoYiBUuXQ=
 github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
 github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
 go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
@@ -92,8 +90,8 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
 golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
-golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
+golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
+golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
diff --git a/pkg/plugin/main.go b/pkg/plugin/main.go
index 01d7ba4..ef5ad31 100644
--- a/pkg/plugin/main.go
+++ b/pkg/plugin/main.go
@@ -12,6 +12,7 @@ import (
 )
 
 //nolint:gochecknoglobals
+//goland:noinspection GoUnusedGlobalVariable
 var (
 	version = "dev"
 	commit  = "none"
diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go
index 43d1200..ef29ee1 100644
--- a/pkg/plugin/plugin.go
+++ b/pkg/plugin/plugin.go
@@ -124,7 +124,7 @@ func openvpn_plugin_func_v3_go(v3structver C.int, args *C.struct_openvpn_plugin_
 		AuthFailedReasonFile: client.AuthFailedReasonFile,
 	}
 	session := state.New(clientIdentifier, client.IpAddr, client.CommonName)
-	if err := session.Encode(handle.conf.HTTP.Secret); err != nil {
+	if err := session.Encode(handle.conf.HTTP.Secret.String()); err != nil {
 		handle.logger.Error(fmt.Errorf("encoding state: %w", err).Error())
 		return C.OPENVPN_PLUGIN_FUNC_ERROR
 	}
diff --git a/renovate.json b/renovate.json
index 6a85e3c..2dc6f8b 100644
--- a/renovate.json
+++ b/renovate.json
@@ -8,5 +8,6 @@
   ],
   "labels": ["dependencies"],
   "dependencyDashboardApproval": true,
-  "postUpdateOptions": ["gomodTidy"]
+  "postUpdateOptions": ["gomodTidy"],
+  "ignoreDeps": ["github.com/jkroepke/openvpn-auth-oauth2"]
 }


Test patch:
diff --git a/cmd/root_test.go b/cmd/root_test.go
index 11580f2..ae115b6 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -27,6 +27,20 @@ func TestExecuteVersion(t *testing.T) {
 	assert.Equal(t, 0, returnCode, buf.String())
 }
 
+func TestExecuteHelp(t *testing.T) {
+	t.Parallel()
+
+	var buf bytes.Buffer
+	_ = io.Writer(&buf)
+
+	returnCode := cmd.Execute([]string{"openvpn-auth-oauth2-test", "--help"}, &buf, "version", "commit", "date")
+	output := buf.String()
+
+	assert.Equal(t, 0, returnCode, buf.String())
+	assert.Contains(t, output, "Usage of openvpn-auth-oauth2-test")
+	assert.Contains(t, output, "--version")
+}
+
 func TestExecuteConfigInvalid(t *testing.T) {
 	t.Parallel()
 
@@ -53,7 +67,7 @@ func TestExecuteConfigInvalid(t *testing.T) {
 		{
 			"invalid log level",
 			[]string{"", "--config=../config.example.yaml", "--log.format=console", "--log.level=invalid", "--http.secret=1234567891111213"},
-			`error configure logging: unable to parse log level: slog: level string \"invalid\": unknown name`,
+			`error parsing cli args: invalid value \"invalid\" for flag -log.level: slog: level string \"invalid\": unknown name`,
 		},
 		{
 			"error oidc provider",
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index e11b689..6728b30 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -1,7 +1,11 @@
 package config_test
 
 import (
+	"bytes"
+	"flag"
+	"io"
 	"net/url"
+	"strings"
 	"testing"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
@@ -37,22 +41,23 @@ func TestFlagSet(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
-			flagSet := config.FlagSet()
-			err := flagSet.Parse(append([]string{"openvpn-auth-oauth2"}, tt.args...))
+			var buf bytes.Buffer
+			_ = io.Writer(&buf)
+
+			flagSet := config.FlagSet("")
+			flagSet.SetOutput(&buf)
+			err := flagSet.Parse(tt.args)
 			require.NoError(t, err)
 			for arg, expected := range tt.expectArgs {
 				switch expectedTyped := expected.(type) {
 				case string:
-					value, err := flagSet.GetString(arg)
-					require.NoError(t, err)
+					value := flagSet.Lookup(arg).Value.String()
 					assert.Equal(t, expectedTyped, value)
 				case bool:
-					value, err := flagSet.GetBool(arg)
-					require.NoError(t, err)
+					value := flagSet.Lookup(arg).Value.String() == "true"
 					assert.Equal(t, expectedTyped, value)
 				case []string:
-					value, err := flagSet.GetStringSlice(arg)
-					require.NoError(t, err)
+					value := strings.Split(flagSet.Lookup(arg).Value.String(), ",")
 					assert.Equal(t, expectedTyped, value)
 				}
 			}
@@ -60,6 +65,20 @@ func TestFlagSet(t *testing.T) {
 	}
 }
 
+func TestFlagSetHelp(t *testing.T) {
+	t.Parallel()
+
+	var buf bytes.Buffer
+	_ = io.Writer(&buf)
+
+	flagSet := config.FlagSet("")
+	flagSet.SetOutput(&buf)
+
+	err := flagSet.Parse([]string{"--help"})
+
+	require.ErrorIs(t, flag.ErrHelp, err)
+}
+
 func TestValidate(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/config/secret_test.go b/internal/config/secret_test.go
new file mode 100644
index 0000000..6751100
--- /dev/null
+++ b/internal/config/secret_test.go
@@ -0,0 +1,49 @@
+package config_test
+
+import (
+	"os"
+	"path"
+	"testing"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSecretString(t *testing.T) {
+	t.Parallel()
+
+	secret := config.Secret("SECRET")
+
+	assert.Equal(t, "SECRET", secret.String())
+}
+
+func TestSecretMarshalText(t *testing.T) {
+	t.Parallel()
+
+	secret, err := config.Secret("SECRET").MarshalText()
+
+	require.NoError(t, err)
+	assert.Equal(t, []byte("SECRET"), secret)
+}
+
+func TestSecretUnmarshalText(t *testing.T) {
+	t.Parallel()
+
+	var secret config.Secret
+
+	require.NoError(t, secret.UnmarshalText([]byte("SECRET")))
+	assert.Equal(t, config.Secret("SECRET"), secret)
+}
+
+func TestSecretUnmarshalTextFile(t *testing.T) {
+	t.Parallel()
+
+	var secret config.Secret
+
+	filePath := path.Join(t.TempDir(), "test.file")
+
+	require.NoError(t, os.WriteFile(filePath, []byte("SECRET"), 0o600))
+	require.NoError(t, secret.UnmarshalText([]byte("file://"+filePath)))
+	assert.Equal(t, config.Secret("SECRET"), secret)
+}
diff --git a/internal/config/slice_test.go b/internal/config/slice_test.go
new file mode 100644
index 0000000..177b941
--- /dev/null
+++ b/internal/config/slice_test.go
@@ -0,0 +1,29 @@
+package config_test
+
+import (
+	"testing"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSliceUnmarshalText(t *testing.T) {
+	t.Parallel()
+
+	var slice config.StringSlice
+
+	require.NoError(t, slice.UnmarshalText([]byte("a,b,c,d")))
+
+	assert.Equal(t, config.StringSlice{"a", "b", "c", "d"}, slice)
+}
+
+func TestSliceMarshalText(t *testing.T) {
+	t.Parallel()
+
+	slice, err := config.StringSlice{"a", "b", "c", "d"}.MarshalText()
+
+	require.NoError(t, err)
+
+	assert.Equal(t, []byte("a,b,c,d"), slice)
+}
diff --git a/internal/oauth2/handler_test.go b/internal/oauth2/handler_test.go
index ed696a8..60c108c 100644
--- a/internal/oauth2/handler_test.go
+++ b/internal/oauth2/handler_test.go
@@ -382,7 +382,7 @@ func TestHandler(t *testing.T) {
 			session := tt.state
 			if tt.state == "-" {
 				sessionState := state.New(state.ClientIdentifier{Cid: 0, Kid: 1}, tt.ipaddr, "name")
-				err = sessionState.Encode(tt.conf.HTTP.Secret)
+				err = sessionState.Encode(tt.conf.HTTP.Secret.String())
 				require.NoError(t, err)
 
 				session = sessionState.Encoded()
diff --git a/internal/oauth2/provider_test.go b/internal/oauth2/provider_test.go
index cbbce44..db12a7d 100644
--- a/internal/oauth2/provider_test.go
+++ b/internal/oauth2/provider_test.go
@@ -159,7 +159,7 @@ func TestNewProvider(t *testing.T) {
 			require.NoError(t, err)
 
 			assert.Equal(t, provider.OAuthConfig().ClientID, tt.config.OAuth2.Client.ID)
-			assert.Equal(t, provider.OAuthConfig().ClientSecret, tt.config.OAuth2.Client.Secret)
+			assert.Equal(t, provider.OAuthConfig().ClientSecret, tt.config.OAuth2.Client.Secret.String())
 			if tt.config.OAuth2.Endpoints.Auth != nil {
 				assert.Equal(t, provider.OAuthConfig().Endpoint.AuthURL, tt.config.OAuth2.Endpoints.Auth.String())
 			} else {
diff --git a/internal/openvpn/main_test.go b/internal/openvpn/main_test.go
index 8609ce4..70fd88d 100644
--- a/internal/openvpn/main_test.go
+++ b/internal/openvpn/main_test.go
@@ -206,7 +206,7 @@ func TestClientFull(t *testing.T) {
 
 				if tt.conf.OpenVpn.Password != "" {
 					testutils.SendLine(t, conn, "ENTER PASSWORD:")
-					assert.Equal(t, tt.conf.OpenVpn.Password, testutils.ReadLine(t, reader))
+					assert.Equal(t, tt.conf.OpenVpn.Password.String(), testutils.ReadLine(t, reader))
 					testutils.SendLine(t, conn, "SUCCESS: password is correct\r\n")
 				}
 
@@ -242,7 +242,7 @@ func TestClientFull(t *testing.T) {
 					assert.Len(t, matches, 2)
 
 					sessionState := state.NewEncoded(matches[1])
-					err := sessionState.Decode(tt.conf.HTTP.Secret)
+					err := sessionState.Decode(tt.conf.HTTP.Secret.String())
 					require.NoError(t, err)
 
 					assert.Equal(t, uint64(1), sessionState.Client.Cid)
@@ -299,7 +299,7 @@ func TestClientInvalidPassword(t *testing.T) {
 		reader := bufio.NewReader(conn)
 
 		testutils.SendLine(t, conn, "ENTER PASSWORD:")
-		assert.Equal(t, conf.OpenVpn.Password, testutils.ReadLine(t, reader))
+		assert.Equal(t, conf.OpenVpn.Password.String(), testutils.ReadLine(t, reader))
 		testutils.SendLine(t, conn, "ERROR: bad password\r\n")
 	}()