Repository: jkroepke/openvpn-auth-oauth2
Pull request #85 (2023-12-08 17:36:18)

Adding Google Workspace / Google Cloud as working provider
Adding Google Workspace / Google Cloud as working provider, out-of-the-box with no special behavior.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/85?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
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`25b34cd`)](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/commit/25b34cd9f7ec86888eca19f6a5a956b18492a7f0?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 [(`1d31453`)](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/85?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) 77.25%.


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


```diff
@@           Coverage Diff           @@
##             main      #85   +/-   ##
=======================================
  Coverage   77.25%   77.25%           
=======================================
  Files          22       22           
  Lines        1433     1433           
=======================================
  Hits         1107     1107           
  Misses        270      270           
  Partials       56       56           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/85?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/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 782c419..2075cf1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -21,7 +21,7 @@ jobs:
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@v4
+      - uses: actions/setup-go@v5
         with:
           go-version-file: 'go.mod'
 
@@ -58,7 +58,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/setup-go@v4
+      - uses: actions/setup-go@v5
         with:
           go-version-file: 'go.mod'
           cache: false
@@ -93,7 +93,7 @@ jobs:
       - uses: actions/checkout@v4
         with:
           fetch-depth: 0
-      - uses: actions/setup-go@v4
+      - uses: actions/setup-go@v5
         with:
           go-version-file: 'go.mod'
       - name: Run GoReleaser
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
index 166f2c8..1d39853 100644
--- a/.github/workflows/stale.yaml
+++ b/.github/workflows/stale.yaml
@@ -12,7 +12,7 @@ jobs:
   stale:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@v8
+      - uses: actions/stale@v9
         with:
           stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
           days-before-stale: 30
diff --git a/.golangci.yaml b/.golangci.yaml
index 3477b2b..8be25c0 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -36,6 +36,7 @@ linters-settings:
     ignore-names:
       - tt
     ignore-decls:
+      - i int
       - err error
       - ok bool
       - w http.ResponseWriter
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/Makefile b/Makefile
index f97dd73..4b10b94 100644
--- a/Makefile
+++ b/Makefile
@@ -58,8 +58,8 @@ goimports:
 
 .PHONY: golangci
 golangci:
-	@go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.1 run .
+	@go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 run ./...
 
 .PHONY: golangci-fix
 golangci-fix:
-	@go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.1 run . --fix
+	@go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 run ./... --fix
diff --git a/README.md b/README.md
index 49683f6..2a29372 100644
--- a/README.md
+++ b/README.md
@@ -8,25 +8,41 @@
 # openvpn-auth-oauth2
 
 openvpn-auth-oauth2 is a management client for OpenVPN that handles the single sign-on (SSO) authentication
-of connecting users against OIDC providers like Azure AD, GitHub or Keycloak.
+of connecting users against OIDC providers like 
+
+* Microsoft Entra ID (Azure AD)
+* GitHub
+* Okta
+* Google Workspace
+* Zittal
+* Digitalocean
+* Keycloak
+* ... any other OIDC compatible auth server
 
 ## Version requirements
 
-- Server: 2.6.2 or later
+- Server: 2.6.2 or later (OpenVPN Access Server not supported)
 - Client: 2.6.0 or later
 
 ## Tested environment
 
 ### Server
 
+#### Working
+
 - OpenVPN 2.6.6 on Linux
 
+#### Non-Working
+
+- OpenVPN Access Server (any)
+
 ### Client
 
 #### Working
 
 - [OpenVPN Community Client for Windows 2.6.0](https://openvpn.net/community-downloads/)
-- [Tunnelblick](https://tunnelblick.net/) [4.0.0beta10+](https://github.com/Tunnelblick/Tunnelblick/issues/676)
+- [Tunnelblick](https://tunnelblick.net/) [4.0.0beta10](https://github.com/Tunnelblick/Tunnelblick/issues/676)
+- [OpenVPN 3 core library 3.9](https://github.com/OpenVPN/openvpn3)
 
 #### Partial Working
 
@@ -56,3 +72,9 @@ https://github.com/jkroepke/openvpn-auth-oauth2/wiki/Configuration
 © [2023 Jan-Otto Kröpke (jkroepke)](https://github.com/jkroepke/openvpn-auth-oauth2)
 
 Licensed under the [MIT License](LICENSE.txt)
+
+# Thanks
+
+- [JetBrains IDEs](https://www.jetbrains.com/?from=jkroepke)
+
+[![JetBrains-Logo (Haupt) logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/?from=jkroepke)
diff --git a/cmd/root.go b/cmd/root.go
index e2feea0..cf36c46 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,26 +52,24 @@ 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
 	}
 
-	provider, err := oauth2.NewProvider(logger, conf)
+	openvpnClient := openvpn.NewClient(logger, conf)
+
+	provider, err := oauth2.NewProvider(logger, conf, openvpnClient)
 	if err != nil {
 		logger.Error(err.Error())
 
 		return 1
 	}
 
-	openvpnClient := openvpn.NewClient(logger, conf)
-	done := make(chan int, 1)
+	server := http.NewHTTPServer(logger, conf, provider.Handler())
 
-	serverHandler := oauth2.Handler(logger, conf, provider, openvpnClient)
-	server := http.NewHTTPServer(logger, conf, serverHandler)
+	done := make(chan int, 1)
 
 	go func() {
 		if err := server.Listen(); err != nil {
@@ -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/docs/Providers.md b/docs/Providers.md
index 7f64fda..9ea1c26 100644
--- a/docs/Providers.md
+++ b/docs/Providers.md
@@ -31,6 +31,27 @@ References:
 - https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
 - https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims
 
+## Google Cloud / Google Workspace
+
+### Register an app on google cloud console
+
+1. Login as admin into your google console from here https://console.cloud.google.com/
+2. click on Create a new project or select an existing project
+3. then "API & Services", then "Credentials" (left column)
+4. click "create credentials" (button at the top-middle) then type "OAuth Client ID" (in the dropdown)
+5. Choose a name for your app like "openvpn connection"
+6. in the "Authorized Redirect URIs" add one and set it to "https://yourdomain/oauth2/callback" (yourdomain would be the domain where a proxy is redirecting to your openvpn-auth-oauth2 daemon, typically on port 9000) 
+7. you'll get your client id and client secret from google, copy them somewhere safe
+8. use those in the config as shown below
+
+### Configuration
+
+Set the following variables in your openvpn-auth-oauth2 configuration file: 
+
+- `CONFIG_OAUTH2_ISSUER=https://accounts.google.com/.well-known/openid-configuration`
+- `CONFIG_OAUTH2_CLIENT_ID=162738495-xxxxx.apps.googleusercontent.com`
+- `CONFIG_OAUTH2_CLIENT_SECRET=GOCSPX-xxxxxxxx`
+
 ## GitHub
 ### Caveats
 A user must explicitly [request](https://help.github.com/articles/requesting-organization-approval-for-oauth-apps/) an
diff --git a/go.mod b/go.mod
index 2778c83..969d85a 100644
--- a/go.mod
+++ b/go.mod
@@ -4,16 +4,17 @@ go 1.21
 
 require (
 	github.com/knadh/koanf/parsers/yaml v0.1.0
+	github.com/knadh/koanf/providers/basicflag v1.0.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.4.0
+	github.com/zitadel/logging v0.5.0
+	github.com/zitadel/oidc/v3 v3.8.0
+	golang.org/x/exp v0.0.0-20231219180239-dc181d75b848
 	golang.org/x/oauth2 v0.15.0
 	golang.org/x/text v0.14.0
 )
@@ -22,12 +23,12 @@ require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
-	github.com/go-chi/chi/v5 v5.0.10 // indirect
+	github.com/go-chi/chi/v5 v5.0.11 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
-	github.com/go-logr/logr v1.3.0 // indirect
+	github.com/go-logr/logr v1.4.1 // 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
@@ -38,15 +39,18 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rs/cors v1.10.1 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
-	github.com/zitadel/logging v0.5.0 // indirect
 	github.com/zitadel/schema v1.3.0 // indirect
 	go.opentelemetry.io/otel v1.21.0 // indirect
 	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/crypto v0.17.0 // 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/mitchellh/mapstructure v1.5.0 => github.com/go-viper/mapstructure v1.6.0
+	github.com/zitadel/oidc/v3 => github.com/jkroepke/oidc/v3 v3.0.0-20231219101841-010f41eefa80
+)
diff --git a/go.sum b/go.sum
index d4217ee..71ea1cb 100644
--- a/go.sum
+++ b/go.sum
@@ -6,15 +6,17 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
-github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
+github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
 github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
-github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-viper/mapstructure v1.6.0 h1:0WdPOF2rmmQDN1xo8qIgxyugvLp71HrZSWyGLxofobw=
+github.com/go-viper/mapstructure v1.6.0/go.mod h1:FcbLReH7/cjaC0RVQR+LHFIrBhHF3s1e/ud1KMDoBVw=
 github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -27,22 +29,24 @@ 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=
 github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
+github.com/jkroepke/oidc/v3 v3.0.0-20231219101841-010f41eefa80 h1:0fCpJVGvH9ozR0n4dftysnR0iV8oPGa8K/E65LkJFyg=
+github.com/jkroepke/oidc/v3 v3.0.0-20231219101841-010f41eefa80/go.mod h1:y3Re3JLoFsE0TPSZg9+eR81XVZNuCwzupmd0oxfqwaU=
 github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
 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 v1.0.0 h1:qB0es/9fYsLuYnrKazxNCuWtkv3JFX1lI1druUsDDvY=
+github.com/knadh/koanf/providers/basicflag v1.0.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=
@@ -55,8 +59,6 @@ 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/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 +71,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=
@@ -79,8 +79,6 @@ 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/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,10 +90,10 @@ go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 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/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
+golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/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..2ad45a7 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -5,6 +5,11 @@ cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdi
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
+github.com/go-viper/mapstructure v0.0.0-20231216201459-8508981c8b6c h1:mjVuHkGWG4b4jHpmrMcQmPjinrSsrQJ5O6ZnECqYABA=
+github.com/go-viper/mapstructure v0.0.0-20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/go-viper/mapstructure v0.0.0-20231218181613-8110e7183d59 h1:awVpHWKpC4A1ktatPcn5whhI4hmnDiNC9CtHv7TD1vs=
+github.com/go-viper/mapstructure v1.6.0 h1:0WdPOF2rmmQDN1xo8qIgxyugvLp71HrZSWyGLxofobw=
+github.com/go-viper/mapstructure v1.6.0/go.mod h1:FcbLReH7/cjaC0RVQR+LHFIrBhHF3s1e/ud1KMDoBVw=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -27,6 +32,7 @@ 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 v1.0.0 h1:qB0es/9fYsLuYnrKazxNCuWtkv3JFX1lI1druUsDDvY=
 github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
 github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -37,6 +43,7 @@ github.com/zitadel/oidc/v2 v2.11.0/go.mod h1:enFSVBQI6aE0TEB1ntjXs9r6O6DEosxX4uh
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
 golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
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..3107f91 100644
--- a/internal/oauth2/handler.go
+++ b/internal/oauth2/handler.go
@@ -11,6 +11,7 @@ import (
 	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/types"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
@@ -24,18 +25,18 @@ type OpenVPN interface {
 	DenyClient(logger *slog.Logger, client state.ClientIdentifier, reason string)
 }
 
-func Handler(logger *slog.Logger, conf config.Config, provider Provider, openvpnCallback OpenVPN) *http.ServeMux {
-	basePath := strings.TrimSuffix(conf.HTTP.BaseURL.Path, "/")
+func (provider Provider) Handler() *http.ServeMux {
+	basePath := strings.TrimSuffix(provider.conf.HTTP.BaseURL.Path, "/")
 
 	mux := http.NewServeMux()
 	mux.Handle("/", http.NotFoundHandler())
-	mux.Handle(utils.StringConcat(basePath, "/oauth2/start"), oauth2Start(logger, provider, conf, openvpnCallback))
-	mux.Handle(utils.StringConcat(basePath, "/oauth2/callback"), oauth2Callback(logger, provider, conf, openvpnCallback))
+	mux.Handle(utils.StringConcat(basePath, "/oauth2/start"), provider.oauth2Start())
+	mux.Handle(utils.StringConcat(basePath, "/oauth2/callback"), provider.oauth2Callback())
 
 	return mux
 }
 
-func oauth2Start(logger *slog.Logger, provider Provider, conf config.Config, openvpn OpenVPN) http.Handler {
+func (provider Provider) oauth2Start() http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		sendCacheHeaders(w)
 
@@ -47,24 +48,24 @@ func oauth2Start(logger *slog.Logger, provider Provider, conf config.Config, ope
 		}
 
 		session := state.NewEncoded(sessionState)
-		if err := session.Decode(conf.HTTP.Secret); err != nil {
-			logger.Warn(utils.StringConcat("invalid state: ", err.Error()))
-			logger.Debug(sessionState)
+		if err := session.Decode(provider.conf.HTTP.Secret.String()); err != nil {
+			provider.logger.Warn(utils.StringConcat("invalid state: ", err.Error()))
+			provider.logger.Debug(sessionState)
 			w.WriteHeader(http.StatusBadRequest)
 
 			return
 		}
 
-		logger = logger.With(
+		logger := provider.logger.With(
 			slog.String("common_name", session.CommonName),
 			slog.Uint64("cid", session.Client.Cid),
 			slog.Uint64("kid", session.Client.Kid),
 		)
 
-		if conf.HTTP.Check.IPAddr {
-			ok, httpStatusCode, denyReason := checkClientIPAddr(r, logger, session, conf)
+		if provider.conf.HTTP.Check.IPAddr {
+			ok, httpStatusCode, denyReason := checkClientIPAddr(r, logger, session, provider.conf)
 			if !ok {
-				openvpn.DenyClient(logger, session.Client, denyReason)
+				provider.openvpn.DenyClient(logger, session.Client, denyReason)
 				w.WriteHeader(httpStatusCode)
 
 				return
@@ -75,7 +76,7 @@ func oauth2Start(logger *slog.Logger, provider Provider, conf config.Config, ope
 
 		rp.AuthURLHandler(func() string {
 			return sessionState
-		}, provider.RelyingParty).ServeHTTP(w, r)
+		}, provider.RelyingParty, provider.authorizeParams...).ServeHTTP(w, r)
 	})
 }
 
@@ -113,11 +114,9 @@ func checkClientIPAddr(r *http.Request, logger *slog.Logger, session state.State
 	return true, 0, ""
 }
 
-func oauth2Callback(
-	logger *slog.Logger, provider Provider, conf config.Config, openvpn OpenVPN,
-) http.Handler {
+func (provider Provider) oauth2Callback() http.Handler {
 	return rp.CodeExchangeHandler(func(
-		w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], encryptedSession string,
+		w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*idtoken.Claims], encryptedSession string,
 		rp rp.RelyingParty,
 	) {
 		w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
@@ -127,6 +126,7 @@ func oauth2Callback(
 		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 		defer cancel()
 
+		logger := provider.logger
 		if tokens.IDTokenClaims != nil {
 			logger = logger.With(
 				slog.String("idtoken.subject", tokens.IDTokenClaims.Subject),
@@ -135,10 +135,10 @@ func oauth2Callback(
 		}
 
 		session := state.NewEncoded(encryptedSession)
-		if err := session.Decode(conf.HTTP.Secret); err != nil {
+		if err := session.Decode(provider.conf.HTTP.Secret.String()); err != nil {
 			logger.Warn(err.Error())
 			logger.Debug(encryptedSession)
-			writeError(w, logger, conf, http.StatusInternalServerError, "invalidSession", err.Error())
+			writeError(w, logger, provider.conf, http.StatusInternalServerError, "invalidSession", err.Error())
 
 			return
 		}
@@ -152,8 +152,8 @@ func oauth2Callback(
 		user, err := provider.OIDC.GetUser(ctx, tokens)
 		if err != nil {
 			logger.Error(err.Error())
-			openvpn.DenyClient(logger, session.Client, "unable to fetch user data")
-			writeError(w, logger, conf, http.StatusInternalServerError, "fetchUser", err.Error())
+			provider.openvpn.DenyClient(logger, session.Client, "unable to fetch user data")
+			writeError(w, logger, provider.conf, http.StatusInternalServerError, "fetchUser", err.Error())
 
 			return
 		}
@@ -167,23 +167,23 @@ func oauth2Callback(
 		if err != nil {
 			reason := err.Error()
 			logger.Warn(reason)
-			openvpn.DenyClient(logger, session.Client, "client rejected")
+			provider.openvpn.DenyClient(logger, session.Client, "client rejected")
 
-			writeError(w, logger, conf, http.StatusInternalServerError, "tokenValidation", reason)
+			writeError(w, logger, provider.conf, http.StatusInternalServerError, "tokenValidation", reason)
 
 			return
 		}
 
 		logger.Info("successful authorization via oauth2")
 
-		if conf.OpenVpn.AuthTokenUser {
+		if provider.conf.OpenVpn.AuthTokenUser {
 			username := getAuthTokenUsername(session, user)
-			openvpn.AcceptClientWithToken(logger, session.Client, username)
+			provider.openvpn.AcceptClientWithToken(logger, session.Client, username)
 		} else {
-			openvpn.AcceptClient(logger, session.Client)
+			provider.openvpn.AcceptClient(logger, session.Client)
 		}
 
-		writeSuccess(w, conf, logger)
+		writeSuccess(w, provider.conf, logger)
 	}, provider.RelyingParty)
 }
 
diff --git a/internal/oauth2/idtoken/main.go b/internal/oauth2/idtoken/main.go
new file mode 100644
index 0000000..01a5cf1
--- /dev/null
+++ b/internal/oauth2/idtoken/main.go
@@ -0,0 +1,16 @@
+package idtoken
+
+import "github.com/zitadel/oidc/v3/pkg/oidc"
+
+//nolint:tagliatelle
+type Claims struct {
+	oidc.TokenClaims
+	PreferredUsername string         `json:"preferred_username,omitempty"`
+	AccessTokenHash   string         `json:"at_hash,omitempty"`
+	Claims            map[string]any `json:"-"`
+}
+
+// GetAccessTokenHash implements the oidc.IDTokenClaims interface.
+func (c *Claims) GetAccessTokenHash() string {
+	return c.AccessTokenHash
+}
diff --git a/internal/oauth2/log/logger.go b/internal/oauth2/log/logger.go
new file mode 100644
index 0000000..6260eda
--- /dev/null
+++ b/internal/oauth2/log/logger.go
@@ -0,0 +1,70 @@
+package log
+
+import (
+	"context"
+	"log/slog"
+
+	expslog "golang.org/x/exp/slog"
+)
+
+type SLogHandler struct {
+	logger      *slog.Logger
+	levelMapper map[expslog.Level]slog.Level
+}
+
+func NewZitadelLogger(logger *slog.Logger) *expslog.Logger {
+	return expslog.New(newSlogHandler(logger))
+}
+
+func newSlogHandler(logger *slog.Logger) *SLogHandler {
+	return &SLogHandler{
+		logger,
+		map[expslog.Level]slog.Level{
+			expslog.LevelDebug: slog.LevelDebug,
+			expslog.LevelInfo:  slog.LevelInfo,
+			expslog.LevelWarn:  slog.LevelWarn,
+			expslog.LevelError: slog.LevelError,
+		},
+	}
+}
+
+func (handler SLogHandler) Enabled(ctx context.Context, level expslog.Level) bool {
+	return handler.logger.Enabled(ctx, handler.convertLevel(level))
+}
+
+func (handler SLogHandler) Handle(ctx context.Context, expRecord expslog.Record) error {
+	record := slog.NewRecord(expRecord.Time, handler.convertLevel(expRecord.Level), expRecord.Message, expRecord.PC)
+
+	return handler.logger.Handler().Handle(ctx, record) //nolint:wrapcheck
+}
+
+func (handler SLogHandler) WithAttrs(attrs []expslog.Attr) expslog.Handler {
+	logger := handler.logger
+	for _, attr := range attrs {
+		logger = logger.With(attr.Key, attr.Value.String())
+	}
+
+	return &SLogHandler{logger, handler.levelMapper}
+}
+
+func (handler SLogHandler) WithGroup(name string) expslog.Handler {
+	return &SLogHandler{handler.logger.WithGroup(name), handler.levelMapper}
+}
+
+func (handler SLogHandler) convertLevel(expLevel expslog.Level) slog.Level {
+	level, ok := handler.levelMapper[expLevel]
+	if ok {
+		return level
+	}
+
+	bytes, err := expLevel.MarshalText()
+	if err != nil {
+		panic(err)
+	}
+
+	if err = level.UnmarshalText(bytes); err != nil {
+		panic(err)
+	}
+
+	return level
+}
diff --git a/internal/oauth2/provider.go b/internal/oauth2/provider.go
index 68a5da2..4d54d8d 100644
--- a/internal/oauth2/provider.go
+++ b/internal/oauth2/provider.go
@@ -5,31 +5,41 @@ import (
 	"fmt"
 	"log/slog"
 	"net/http"
-	"strings"
+	"net/url"
+	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/log"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/generic"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/github"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
+	"github.com/zitadel/logging"
 	"github.com/zitadel/oidc/v3/pkg/client/rp"
 	httphelper "github.com/zitadel/oidc/v3/pkg/http"
 	"golang.org/x/oauth2"
 )
 
-// NewProvider returns a [rp.RelyingParty] instance.
-func NewProvider(logger *slog.Logger, conf config.Config) (Provider, error) {
+// NewProvider returns a [Provider] instance.
+func NewProvider(logger *slog.Logger, conf config.Config, openvpnCallback OpenVPN) (Provider, error) {
 	provider, err := newOidcProvider(conf)
 	if err != nil {
 		return Provider{}, err
 	}
 
-	redirectURI := utils.StringConcat(strings.TrimSuffix(conf.HTTP.BaseURL.String(), "/"), "/oauth2/callback")
+	authorizeParams, err := GetAuthorizeParams(conf.OAuth2.AuthorizeParams)
+	if err != nil {
+		return Provider{}, err
+	}
+
+	basePath := conf.HTTP.BaseURL.JoinPath("/oauth2")
+	redirectURI := basePath.JoinPath("/callback").String()
 
 	cookieKey := []byte(conf.HTTP.Secret)
 	cookieOpt := []httphelper.CookieHandlerOpt{
-		httphelper.WithMaxAge(1800),
-		httphelper.WithPath(utils.StringConcat(strings.TrimSuffix(conf.HTTP.BaseURL.Path, "/"), "/oauth2")),
-		httphelper.WithDomain(conf.HTTP.BaseURL.Hostname()),
+		httphelper.WithMaxAge(int(conf.OpenVpn.AuthPendingTimeout.Seconds()) + 5),
+		httphelper.WithPath(basePath.Path),
+		httphelper.WithDomain(basePath.Hostname()),
 	}
 
 	if conf.HTTP.BaseURL.Scheme == "http" {
@@ -37,22 +47,18 @@ func NewProvider(logger *slog.Logger, conf config.Config) (Provider, error) {
 	}
 
 	cookieHandler := httphelper.NewCookieHandler(cookieKey, cookieKey, cookieOpt...)
+	providerLogger := log.NewZitadelLogger(logger)
 
 	options := []rp.Option{
+		rp.WithLogger(providerLogger),
 		rp.WithCookieHandler(cookieHandler),
-		rp.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
-			if conf.HTTP.CallbackTemplate == nil {
-				http.Error(w, errorType+": "+errorDesc, http.StatusInternalServerError)
-			} else {
-				err := conf.HTTP.CallbackTemplate.Execute(w, map[string]string{
-					"errorDesc": errorDesc,
-					"errorType": errorType,
-				})
-				if err != nil {
-					logger.Error("executing template:", err)
-					w.WriteHeader(http.StatusInternalServerError)
-				}
-			}
+		rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
+		rp.WithHTTPClient(&http.Client{Timeout: time.Second * 30, Transport: utils.NewUserAgentTransport(nil)}),
+		rp.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, encryptedSession string) {
+			errorHandler(w, conf, logger, openvpnCallback, http.StatusInternalServerError, errorType, errorDesc, encryptedSession)
+		}),
+		rp.WithUnauthorizedHandler(func(w http.ResponseWriter, r *http.Request, desc string, encryptedSession string) {
+			errorHandler(w, conf, logger, openvpnCallback, http.StatusUnauthorized, "Unauthorized", desc, encryptedSession)
 		}),
 	}
 
@@ -70,76 +76,83 @@ func NewProvider(logger *slog.Logger, conf config.Config) (Provider, error) {
 		scopes = provider.GetDefaultScopes()
 	}
 
+	var relyingParty rp.RelyingParty
+
 	if endpoints == (oauth2.Endpoint{}) {
 		if !config.IsURLEmpty(conf.OAuth2.Endpoints.Discovery) {
-			logger.Info(utils.StringConcat(
-				"discover oidc auto configuration with provider ",
-				provider.GetName(), " for issuer ", conf.OAuth2.Issuer.String(),
-				"with custom discovery url", conf.OAuth2.Endpoints.Discovery.String(),
+			logger.Info(fmt.Sprintf(
+				"discover oidc auto configuration with provider %s for issuer %s with custom discovery url %s",
+				provider.GetName(), conf.OAuth2.Issuer.String(), conf.OAuth2.Endpoints.Discovery.String(),
 			))
 
 			options = append(options, rp.WithCustomDiscoveryUrl(conf.OAuth2.Endpoints.Discovery.String()))
 		} else {
-			logger.Info(utils.StringConcat(
-				"discover oidc auto configuration with provider ",
-				provider.GetName(), " for issuer ", conf.OAuth2.Issuer.String(),
+			logger.Info(fmt.Sprintf(
+				"discover oidc auto configuration with provider %s for issuer %s",
+				provider.GetName(), conf.OAuth2.Issuer.String(),
 			))
 		}
 
-		return newProviderWithDiscovery(conf, logger, provider, options, redirectURI, scopes)
-	}
-
-	logger.Info(utils.StringConcat(
-		"manually configure oauth2 provider with provider ",
-		provider.GetName(), " and endpoints ", endpoints.AuthURL, " and ", endpoints.TokenURL,
-	))
-
-	return newProviderWithEndpoints(conf, provider, options, redirectURI, endpoints, scopes)
-}
+		relyingParty, err = rp.NewRelyingPartyOIDC(
+			logging.ToContext(context.Background(), providerLogger),
+			conf.OAuth2.Issuer.String(),
+			conf.OAuth2.Client.ID,
+			conf.OAuth2.Client.Secret.String(),
+			redirectURI,
+			scopes,
+			options...,
+		)
+	} else {
+		logger.Info(utils.StringConcat(
+			"manually configure oauth2 provider with provider ",
+			provider.GetName(), " and endpoints ", endpoints.AuthURL, " and ", endpoints.TokenURL,
+		))
+
+		rpConfig := &oauth2.Config{
+			ClientID:     conf.OAuth2.Client.ID,
+			ClientSecret: conf.OAuth2.Client.Secret.String(),
+			RedirectURL:  redirectURI,
+			Scopes:       scopes,
+			Endpoint:     endpoints,
+		}
 
-func newProviderWithEndpoints(
-	conf config.Config, provider oidcProvider, options []rp.Option, redirectURI string, endpoints oauth2.Endpoint, scopes []string,
-) (Provider, error) {
-	rpConfig := &oauth2.Config{
-		ClientID:     conf.OAuth2.Client.ID,
-		ClientSecret: conf.OAuth2.Client.Secret,
-		RedirectURL:  redirectURI,
-		Scopes:       scopes,
-		Endpoint:     endpoints,
+		relyingParty, err = rp.NewRelyingPartyOAuth(rpConfig, options...)
 	}
 
-	relayingParty, err := rp.NewRelyingPartyOAuth(rpConfig, options...)
 	if err != nil {
-		return Provider{}, fmt.Errorf("newProviderWithEndpoints: %w", err)
+		return Provider{}, fmt.Errorf("error oauth2 provider: %w", err)
 	}
 
 	return Provider{
-		RelyingParty: relayingParty,
-		OIDC:         provider,
+		RelyingParty:    relyingParty,
+		OIDC:            provider,
+		openvpn:         openvpnCallback,
+		conf:            conf,
+		logger:          logger,
+		authorizeParams: authorizeParams,
 	}, nil
 }
 
-func newProviderWithDiscovery(
-	conf config.Config, _ *slog.Logger, provider oidcProvider, options []rp.Option, redirectURI string, scopes []string,
-) (Provider, error) {
-	// expLogger := logging.ToContext(context.Background(), logger)s
-	relayingParty, err := rp.NewRelyingPartyOIDC(
-		context.Background(),
-		conf.OAuth2.Issuer.String(),
-		conf.OAuth2.Client.ID,
-		conf.OAuth2.Client.Secret,
-		redirectURI,
-		scopes,
-		options...,
-	)
+func GetAuthorizeParams(authorizeParams string) ([]rp.URLParamOpt, error) {
+	authorizeParamsQuery, err := url.ParseQuery(authorizeParams)
 	if err != nil {
-		return Provider{}, fmt.Errorf("newProviderWithDiscovery: %w", err)
+		return nil, fmt.Errorf("unable to parse '%s': %w", authorizeParams, err)
 	}
 
-	return Provider{
-		RelyingParty: relayingParty,
-		OIDC:         provider,
-	}, nil
+	params := make([]rp.URLParamOpt, len(authorizeParamsQuery))
+
+	var i int
+
+	for key, value := range authorizeParamsQuery {
+		if len(value) == 0 {
+			return nil, fmt.Errorf("authorize param %s does not have values", key)
+		}
+
+		params[i] = rp.WithURLParam(key, value[0])
+		i++
+	}
+
+	return params, nil
 }
 
 func newOidcProvider(conf config.Config) (oidcProvider, error) {
@@ -152,3 +165,23 @@ func newOidcProvider(conf config.Config) (oidcProvider, error) {
 		return nil, fmt.Errorf("unknown oauth2 provider: %s", conf.OAuth2.Provider)
 	}
 }
+
+func errorHandler(
+	w http.ResponseWriter, conf config.Config, logger *slog.Logger, openvpn OpenVPN,
+	httpStatus int, errorType string, errorDesc string, encryptedSession string,
+) {
+	session := state.NewEncoded(encryptedSession)
+	if err := session.Decode(conf.HTTP.Secret.String()); err == nil {
+		logger = logger.With(
+			slog.String("common_name", session.CommonName),
+			slog.Uint64("cid", session.Client.Cid),
+			slog.Uint64("kid", session.Client.Kid),
+		)
+		openvpn.DenyClient(logger, session.Client, "client rejected")
+	} else {
+		logger.Debug(fmt.Sprintf("errorHandler: %s", err.Error()))
+	}
+
+	logger.Warn(fmt.Sprintf("%s: %s", errorType, errorDesc))
+	writeError(w, logger, conf, httpStatus, errorType, errorDesc)
+}
diff --git a/internal/oauth2/providers/generic/check.go b/internal/oauth2/providers/generic/check.go
index 1f6abc8..71967a0 100644
--- a/internal/oauth2/providers/generic/check.go
+++ b/internal/oauth2/providers/generic/check.go
@@ -6,6 +6,7 @@ import (
 	"slices"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/types"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
@@ -16,7 +17,7 @@ func (p *Provider) CheckUser(
 	_ context.Context,
 	session state.State,
 	_ types.UserData,
-	tokens *oidc.Tokens[*oidc.IDTokenClaims],
+	tokens *oidc.Tokens[*idtoken.Claims],
 ) error {
 	if err := p.CheckGroups(tokens); err != nil {
 		return err
@@ -33,7 +34,7 @@ func (p *Provider) CheckUser(
 	return p.CheckIPAddress(session, tokens)
 }
 
-func (p *Provider) CheckGroups(tokens *oidc.Tokens[*oidc.IDTokenClaims]) error {
+func (p *Provider) CheckGroups(tokens *oidc.Tokens[*idtoken.Claims]) error {
 	if len(p.Conf.OAuth2.Validate.Groups) == 0 {
 		return nil
 	}
@@ -57,7 +58,7 @@ func (p *Provider) CheckGroups(tokens *oidc.Tokens[*oidc.IDTokenClaims]) error {
 	return nil
 }
 
-func (p *Provider) CheckRoles(tokens *oidc.Tokens[*oidc.IDTokenClaims]) error {
+func (p *Provider) CheckRoles(tokens *oidc.Tokens[*idtoken.Claims]) error {
 	if len(p.Conf.OAuth2.Validate.Roles) == 0 {
 		return nil
 	}
@@ -81,7 +82,7 @@ func (p *Provider) CheckRoles(tokens *oidc.Tokens[*oidc.IDTokenClaims]) error {
 	return nil
 }
 
-func (p *Provider) CheckCommonName(session state.State, tokens *oidc.Tokens[*oidc.IDTokenClaims]) error {
+func (p *Provider) CheckCommonName(session state.State, tokens *oidc.Tokens[*idtoken.Claims]) error {
 	if p.Conf.OAuth2.Validate.CommonName == "" {
 		return nil
 	}
@@ -104,7 +105,7 @@ func (p *Provider) CheckCommonName(session state.State, tokens *oidc.Tokens[*oid
 	return nil
 }
 
-func (p *Provider) CheckIPAddress(session state.State, tokens *oidc.Tokens[*oidc.IDTokenClaims]) error {
+func (p *Provider) CheckIPAddress(session state.State, tokens *oidc.Tokens[*idtoken.Claims]) error {
 	if !p.Conf.OAuth2.Validate.IPAddr {
 		return nil
 	}
diff --git a/internal/oauth2/providers/generic/user.go b/internal/oauth2/providers/generic/user.go
index 17dbcf5..a7aa5e9 100644
--- a/internal/oauth2/providers/generic/user.go
+++ b/internal/oauth2/providers/generic/user.go
@@ -3,11 +3,12 @@ package generic
 import (
 	"context"
 
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/types"
 	"github.com/zitadel/oidc/v3/pkg/oidc"
 )
 
-func (p *Provider) GetUser(_ context.Context, tokens *oidc.Tokens[*oidc.IDTokenClaims]) (types.UserData, error) {
+func (p *Provider) GetUser(_ context.Context, tokens *oidc.Tokens[*idtoken.Claims]) (types.UserData, error) {
 	var (
 		preferredUsername string
 		subject           string
diff --git a/internal/oauth2/providers/github/check.go b/internal/oauth2/providers/github/check.go
index a37bdec..8c6b040 100644
--- a/internal/oauth2/providers/github/check.go
+++ b/internal/oauth2/providers/github/check.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/types"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
@@ -21,10 +22,10 @@ type teamType struct {
 }
 
 func (p *Provider) CheckUser(
-	ctx context.Context, _ state.State, _ types.UserData, tokens *oidc.Tokens[*oidc.IDTokenClaims],
+	ctx context.Context, _ state.State, _ types.UserData, tokens *oidc.Tokens[*idtoken.Claims],
 ) error {
 	//nolint: exhaustruct
-	tokens.IDTokenClaims = &oidc.IDTokenClaims{
+	tokens.IDTokenClaims = &idtoken.Claims{
 		Claims: make(map[string]any),
 	}
 
@@ -35,7 +36,7 @@ func (p *Provider) CheckUser(
 	return p.CheckTeams(ctx, tokens)
 }
 
-func (p *Provider) CheckTeams(ctx context.Context, tokens *oidc.Tokens[*oidc.IDTokenClaims]) error {
+func (p *Provider) CheckTeams(ctx context.Context, tokens *oidc.Tokens[*idtoken.Claims]) error {
 	if len(p.Provider.Conf.OAuth2.Validate.Roles) != 0 {
 		return nil
 	}
@@ -72,7 +73,7 @@ func (p *Provider) CheckTeams(ctx context.Context, tokens *oidc.Tokens[*oidc.IDT
 	return nil
 }
 
-func (p *Provider) CheckOrgs(ctx context.Context, tokens *oidc.Tokens[*oidc.IDTokenClaims]) error {
+func (p *Provider) CheckOrgs(ctx context.Context, tokens *oidc.Tokens[*idtoken.Claims]) error {
 	if len(p.Provider.Conf.OAuth2.Validate.Groups) != 0 {
 		return nil
 	}
diff --git a/internal/oauth2/providers/github/user.go b/internal/oauth2/providers/github/user.go
index 16c9fca..f066558 100644
--- a/internal/oauth2/providers/github/user.go
+++ b/internal/oauth2/providers/github/user.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"strconv"
 
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/types"
 	"github.com/zitadel/oidc/v3/pkg/oidc"
 )
@@ -17,7 +18,7 @@ type userType struct {
 	Email string `json:"email"`
 }
 
-func (p *Provider) GetUser(ctx context.Context, tokens *oidc.Tokens[*oidc.IDTokenClaims]) (types.UserData, error) {
+func (p *Provider) GetUser(ctx context.Context, tokens *oidc.Tokens[*idtoken.Claims]) (types.UserData, error) {
 	var user userType
 
 	_, err := get[userType](ctx, tokens.AccessToken, "https://api.github.com/user", &user)
diff --git a/internal/oauth2/types.go b/internal/oauth2/types.go
index 95e9315..b0675bb 100644
--- a/internal/oauth2/types.go
+++ b/internal/oauth2/types.go
@@ -2,8 +2,10 @@ package oauth2
 
 import (
 	"context"
+	"log/slog"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/types"
 	"github.com/zitadel/oidc/v3/pkg/client/rp"
@@ -12,14 +14,19 @@ import (
 )
 
 type Provider struct {
+	conf   config.Config
+	logger *slog.Logger
+
 	rp.RelyingParty
-	OIDC oidcProvider
+	OIDC            oidcProvider
+	openvpn         OpenVPN
+	authorizeParams []rp.URLParamOpt
 }
 
 type oidcProvider interface {
-	CheckUser(ctx context.Context, session state.State, user types.UserData, tokens *oidc.Tokens[*oidc.IDTokenClaims]) error
+	CheckUser(ctx context.Context, session state.State, user types.UserData, tokens *oidc.Tokens[*idtoken.Claims]) error
 	GetDefaultScopes() []string
 	GetEndpoints(conf config.Config) (oauth2.Endpoint, error)
 	GetName() string
-	GetUser(ctx context.Context, tokens *oidc.Tokens[*oidc.IDTokenClaims]) (types.UserData, error)
+	GetUser(ctx context.Context, tokens *oidc.Tokens[*idtoken.Claims]) (types.UserData, error)
 }
diff --git a/internal/openvpn/callbacks.go b/internal/openvpn/callbacks.go
index e579cc9..c4d1299 100644
--- a/internal/openvpn/callbacks.go
+++ b/internal/openvpn/callbacks.go
@@ -1,12 +1,15 @@
 package openvpn
 
 import (
+	"fmt"
 	"log/slog"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 )
 
 func (c *Client) AcceptClient(logger *slog.Logger, client state.ClientIdentifier) {
+	logger.Info(fmt.Sprintf("accept OpenVPN client cid %d, kid %d", client.Cid, client.Kid))
+
 	_, err := c.SendCommandf(`client-auth-nt %d %d`, client.Cid, client.Kid)
 	if err != nil {
 		logger.Warn(err.Error())
@@ -14,6 +17,8 @@ func (c *Client) AcceptClient(logger *slog.Logger, client state.ClientIdentifier
 }
 
 func (c *Client) AcceptClientWithToken(logger *slog.Logger, client state.ClientIdentifier, username string) {
+	logger.Info(fmt.Sprintf("accept OpenVPN client cid %d, kid %d", client.Cid, client.Kid))
+
 	_, err := c.SendCommandf("client-auth %d %d\npush \"auth-token-user %s\"\nEND", client.Cid, client.Kid, username)
 	if err != nil {
 		logger.Warn(err.Error())
@@ -21,6 +26,8 @@ func (c *Client) AcceptClientWithToken(logger *slog.Logger, client state.ClientI
 }
 
 func (c *Client) DenyClient(logger *slog.Logger, client state.ClientIdentifier, reason string) {
+	logger.Info(fmt.Sprintf("deny OpenVPN client cid %d, kid %d", client.Cid, client.Kid))
+
 	_, err := c.SendCommandf(`client-deny %d %d "%s"`, client.Cid, client.Kid, reason)
 	if err != nil {
 		logger.Warn(err.Error())
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/internal/utils/http.go b/internal/utils/http.go
new file mode 100644
index 0000000..f558aff
--- /dev/null
+++ b/internal/utils/http.go
@@ -0,0 +1,21 @@
+package utils
+
+import "net/http"
+
+type UserAgentTransport struct {
+	rt http.RoundTripper
+}
+
+func NewUserAgentTransport(rt http.RoundTripper) *UserAgentTransport {
+	if rt == nil {
+		rt = http.DefaultTransport
+	}
+
+	return &UserAgentTransport{rt}
+}
+
+func (adt *UserAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	req.Header.Add("User-Agent", "openvpn-auth-oauth2")
+
+	return adt.rt.RoundTrip(req) //nolint: wrapcheck
+}
diff --git a/pkg/plugin/go.mod b/pkg/plugin/go.mod
index e75df35..b8715f8 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 v1.0.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.8.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/crypto v0.17.0 // indirect
+	golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // 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..fb96248 100644
--- a/pkg/plugin/go.sum
+++ b/pkg/plugin/go.sum
@@ -5,8 +5,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
-github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
+github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
 github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
@@ -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 v1.0.0 h1:qB0es/9fYsLuYnrKazxNCuWtkv3JFX1lI1druUsDDvY=
+github.com/knadh/koanf/providers/basicflag v1.0.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.8.0 h1:4Nvok+e6o3FDpqrf14JOg4EVBvwXNFOI1lFHPZU75iA=
+github.com/zitadel/oidc/v3 v3.8.0/go.mod h1:v+aHyg4lBAUuuUHINwXqHtKunPJZo8kPvMpRRBYEKHY=
 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=
@@ -90,10 +88,10 @@ go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 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/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
+golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/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..142e6dd 100644
--- a/pkg/plugin/plugin.go
+++ b/pkg/plugin/plugin.go
@@ -74,15 +74,14 @@ func openvpn_plugin_open_v3_go(v3structver C.int, args *C.struct_openvpn_plugin_
 		conf:   conf,
 	}
 
-	provider, err := oauth2.NewProvider(logger, conf)
+	provider, err := oauth2.NewProvider(logger, conf, handle)
 	if err != nil {
 		logger.Error(err.Error())
 
 		return C.OPENVPN_PLUGIN_FUNC_ERROR
 	}
 
-	serverHandler := oauth2.Handler(logger, conf, provider, handle)
-	handle.server = http.NewHTTPServer(logger, conf, serverHandler)
+	handle.server = http.NewHTTPServer(logger, conf, provider.Handler())
 	go func() {
 		if err := handle.server.Listen(); err != nil {
 			logger.Error(fmt.Errorf("error http listener: %w", err).Error())
@@ -124,7 +123,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 ece8679..2dc6f8b 100644
--- a/renovate.json
+++ b/renovate.json
@@ -7,5 +7,7 @@
     "before 3am on the first day of the month"
   ],
   "labels": ["dependencies"],
-  "postUpdateOptions": ["gomodTidy"]
+  "dependencyDashboardApproval": true,
+  "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..45a5b45 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,12 +67,12 @@ 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",
 			[]string{"", "--config=../config.example.yaml", "--log.format=console", "--log.level=info", "--http.secret=1234567891111213"},
-			`newProviderWithDiscovery`,
+			`error oauth2 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..d5fcc45 100644
--- a/internal/oauth2/handler_test.go
+++ b/internal/oauth2/handler_test.go
@@ -320,10 +320,10 @@ func TestHandler(t *testing.T) {
 			client := openvpn.NewClient(logger, tt.conf)
 			defer client.Shutdown()
 
-			provider, err := oauth2.NewProvider(logger, tt.conf)
+			provider, err := oauth2.NewProvider(logger, tt.conf, client)
 			require.NoError(t, err)
 
-			httpClientListener := httptest.NewUnstartedServer(oauth2.Handler(logger, tt.conf, provider, client))
+			httpClientListener := httptest.NewUnstartedServer(provider.Handler())
 			httpClientListener.Listener.Close()
 			httpClientListener.Listener = clientListener
 			httpClientListener.Start()
@@ -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..bbca164 100644
--- a/internal/oauth2/provider_test.go
+++ b/internal/oauth2/provider_test.go
@@ -2,6 +2,7 @@ package oauth2_test
 
 import (
 	"crypto/sha256"
+	"net"
 	"net/http/httptest"
 	"net/url"
 	"strings"
@@ -10,6 +11,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/generic"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -43,9 +45,9 @@ func TestNewProvider(t *testing.T) {
 	svrURL, _ := url.Parse(svr.URL)
 
 	tests := []struct {
-		name   string
-		config config.Config
-		err    string
+		name string
+		conf config.Config
+		err  string
 	}{
 		{
 			"default",
@@ -88,7 +90,7 @@ func TestNewProvider(t *testing.T) {
 					},
 				},
 			},
-			"newProviderWithDiscovery: http status not ok: 404 Not Found 404 page not found",
+			"error oauth2 provider: http status not ok: 404 Not Found 404 page not found",
 		},
 		{
 			"with custom endpoints",
@@ -148,7 +150,15 @@ func TestNewProvider(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
-			provider, err := oauth2.NewProvider(logger, tt.config)
+			managementInterface, err := net.Listen("tcp", "127.0.0.1:0")
+			require.NoError(t, err)
+			defer managementInterface.Close()
+			tt.conf.OpenVpn.Addr = &url.URL{Scheme: managementInterface.Addr().Network(), Host: managementInterface.Addr().String()}
+
+			client := openvpn.NewClient(logger, tt.conf)
+			defer client.Shutdown()
+
+			provider, err := oauth2.NewProvider(logger, tt.conf, client)
 			if tt.err != "" {
 				require.Error(t, err)
 				assert.Equal(t, strings.TrimSpace(err.Error()), tt.err)
@@ -158,15 +168,15 @@ 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)
-			if tt.config.OAuth2.Endpoints.Auth != nil {
-				assert.Equal(t, provider.OAuthConfig().Endpoint.AuthURL, tt.config.OAuth2.Endpoints.Auth.String())
+			assert.Equal(t, provider.OAuthConfig().ClientID, tt.conf.OAuth2.Client.ID)
+			assert.Equal(t, provider.OAuthConfig().ClientSecret, tt.conf.OAuth2.Client.Secret.String())
+			if tt.conf.OAuth2.Endpoints.Auth != nil {
+				assert.Equal(t, provider.OAuthConfig().Endpoint.AuthURL, tt.conf.OAuth2.Endpoints.Auth.String())
 			} else {
 				assert.NotEmpty(t, provider.OAuthConfig().Endpoint.AuthURL)
 			}
-			if tt.config.OAuth2.Endpoints.Token != nil {
-				assert.Equal(t, provider.OAuthConfig().Endpoint.TokenURL, tt.config.OAuth2.Endpoints.Token.String())
+			if tt.conf.OAuth2.Endpoints.Token != nil {
+				assert.Equal(t, provider.OAuthConfig().Endpoint.TokenURL, tt.conf.OAuth2.Endpoints.Token.String())
 			} else {
 				assert.NotEmpty(t, provider.OAuthConfig().Endpoint.TokenURL)
 			}
diff --git a/internal/oauth2/providers/generic/check_test.go b/internal/oauth2/providers/generic/check_test.go
index 3ca6bdf..1d4d96c 100644
--- a/internal/oauth2/providers/generic/check_test.go
+++ b/internal/oauth2/providers/generic/check_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/generic"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
 	"github.com/stretchr/testify/assert"
@@ -15,15 +16,13 @@ import (
 func TestCheckUser(t *testing.T) {
 	t.Parallel()
 
-	token := &oidc.Tokens[*oidc.IDTokenClaims]{
-		IDTokenClaims: &oidc.IDTokenClaims{
+	token := &oidc.Tokens[*idtoken.Claims]{
+		IDTokenClaims: &idtoken.Claims{
 			TokenClaims: oidc.TokenClaims{
 				Subject: "subnect",
 			},
-			UserInfoProfile: oidc.UserInfoProfile{
-				PreferredUsername: "username",
-			},
-			Claims: map[string]any{},
+			PreferredUsername: "username",
+			Claims:            map[string]any{},
 		},
 	}
 
@@ -66,8 +65,8 @@ func TestValidateGroups(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
-			token := &oidc.Tokens[*oidc.IDTokenClaims]{
-				IDTokenClaims: &oidc.IDTokenClaims{
+			token := &oidc.Tokens[*idtoken.Claims]{
+				IDTokenClaims: &idtoken.Claims{
 					Claims: map[string]any{
 						tt.tokenClaim: tt.tokenGroups,
 					},
@@ -118,8 +117,8 @@ func TestValidateRoles(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
-			token := &oidc.Tokens[*oidc.IDTokenClaims]{
-				IDTokenClaims: &oidc.IDTokenClaims{
+			token := &oidc.Tokens[*idtoken.Claims]{
+				IDTokenClaims: &idtoken.Claims{
 					Claims: map[string]any{
 						tt.tokenClaim: tt.tokenRoles,
 					},
@@ -167,8 +166,8 @@ func TestValidateCommonName(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
-			token := &oidc.Tokens[*oidc.IDTokenClaims]{
-				IDTokenClaims: &oidc.IDTokenClaims{
+			token := &oidc.Tokens[*idtoken.Claims]{
+				IDTokenClaims: &idtoken.Claims{
 					Claims: map[string]any{
 						"sub": tt.tokenCommonName,
 					},
@@ -225,8 +224,8 @@ func TestValidateIpAddr(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
-			token := &oidc.Tokens[*oidc.IDTokenClaims]{
-				IDTokenClaims: &oidc.IDTokenClaims{
+			token := &oidc.Tokens[*idtoken.Claims]{
+				IDTokenClaims: &idtoken.Claims{
 					Claims: map[string]any{
 						tt.tokenClaim: tt.tokenIPAddr,
 					},
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")
 	}()
 
diff --git a/internal/utils/http_test.go b/internal/utils/http_test.go
new file mode 100644
index 0000000..fddddce
--- /dev/null
+++ b/internal/utils/http_test.go
@@ -0,0 +1,38 @@
+package utils_test
+
+import (
+	"context"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestNewUserAgentTransport(t *testing.T) {
+	t.Parallel()
+
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte(r.UserAgent()))
+	}))
+
+	server.Client().Transport = utils.NewUserAgentTransport(nil)
+
+	ctx := context.Background()
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, server.URL, nil)
+
+	require.NoError(t, err)
+
+	resp, err := server.Client().Do(req)
+	require.NoError(t, err)
+
+	defer resp.Body.Close()
+
+	body, err := io.ReadAll(resp.Body)
+	require.NoError(t, err)
+
+	assert.Equal(t, "openvpn-auth-oauth2", string(body))
+}