Repository: jkroepke/openvpn-auth-oauth2
Pull request #41 (2023-10-04 14:00:04)

OpenVPN Plugin
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>**What this PR does / why we need it**:

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #

**Special notes for your reviewer**:

**Particularly user-facing changes**:

**PR Readiness Checklist**:

Complete these before marking the PR as `ready to review`:

- [ ] The PR title has a summary of the changes
- [ ] The PR body has a summary to reflect any significant (and particularly user-facing) changes introduced by this PR


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/41?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) Report
Attention: `2 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`532bc9f`)](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/commit/532bc9f3a89858c3df0f33fc516305a9a4bec95e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) 71.78% compared to head [(`f84bafd`)](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/41?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) 71.71%.

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


```diff
@@            Coverage Diff             @@
##             main      #41      +/-   ##
==========================================
- Coverage   71.78%   71.71%   -0.08%     
==========================================
  Files          19       19              
  Lines        1276     1248      -28     
==========================================
- Hits          916      895      -21     
+ Misses        315      308       -7     
  Partials       45       45              
```


| [Files](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/41?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke) | Coverage Δ | |
|---|---|---|
| [internal/oauth2/handler.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/41?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvb2F1dGgyL2hhbmRsZXIuZ28=) | `54.19% <100.00%> (ø)` | |
| [internal/state/state.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/41?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-aW50ZXJuYWwvc3RhdGUvc3RhdGUuZ28=) | `84.61% <ø> (ø)` | |
| [cmd/root.go](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/41?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke#diff-Y21kL3Jvb3QuZ28=) | `75.72% <50.00%> (+4.73%)` | :arrow_up: |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/41/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Jan-Otto+Kr%C3%B6pke)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jkroepke/openvpn-auth-oauth2/pull/41?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 41c1fd3..3867e57 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -1,8 +1,5 @@
 name: CI
 
-env:
-  CGO: 0
-
 on:
   workflow_dispatch:
   pull_request:
@@ -25,8 +22,19 @@ jobs:
         with:
           go-version-file: 'go.mod'
 
+      - run: sudo apt install openvpn -y
+
       - run: go build
       - run: go test ./... -race -covermode=atomic -coverprofile=coverage.out
+
+      - name: go build (with goreleaser)
+        uses: goreleaser/goreleaser-action@v5
+        with:
+          version: latest
+          args: build --snapshot
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
       - uses: codecov/codecov-action@v3
 
   lint:
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index e5666be..6ea9e38 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -1,5 +1,6 @@
 builds:
-  - binary: openvpn-auth-oauth2
+  - id: "openvpn-auth-oauth2"
+    binary: openvpn-auth-oauth2
     env:
       - CGO_ENABLED=0
     goos:
@@ -8,7 +9,18 @@ builds:
       - amd64
       - arm64
     mod_timestamp: '{{ .CommitTimestamp }}'
-    no_unique_dist_dir: false
+  - id: "openvpn-auth-oauth2.so"
+    binary: openvpn-auth-oauth2
+    main: ./plugin/
+    buildmode: c-shared
+    env:
+      - CGO_ENABLED=1
+    goos:
+      - linux
+    goarch:
+      - amd64
+    mod_timestamp: '{{ .CommitTimestamp }}'
+
 nfpms:
   - id: openvpn-auth-oauth2
     homepage: https://github.com/jkroepke/openvpn-auth-oauth2
@@ -16,6 +28,8 @@ nfpms:
     description: |
       openvpn-auth-oauth2 is a management client for OpenVPN that handles the authentication of connecting users against OIDC providers like Azure AD or Keycloak.
     license: MIT License
+    builds:
+      - openvpn-auth-oauth2
     formats:
       - deb
       - rpm
@@ -26,8 +40,16 @@ nfpms:
         dst: /etc/sysconfig/openvpn-auth-oauth2
         type: "config|noreplace"
 
-    scripts:
-      postinstall: "packaging/postinstall.sh"
-      preremove: "packaging/preremove.sh"
+  - id: openvpn-auth-oauth2-plugin
+    homepage: https://github.com/jkroepke/openvpn-auth-oauth2
+    maintainer: Jan-Otto Kröpke <github@jkroepke.de>
+    description: |
+      openvpn-auth-oauth2 is a management client for OpenVPN that handles the authentication of connecting users against OIDC providers like Azure AD or Keycloak.
+    license: MIT License
+    builds:
+      - openvpn-auth-oauth2.so
+    formats:
+      - deb
+      - rpm
 changelog:
   use: github-native
diff --git a/cmd/root.go b/cmd/root.go
index db4ec1e..874d883 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,19 +1,16 @@
 package cmd
 
 import (
-	"context"
-	"errors"
 	"fmt"
 	"io"
 	"log/slog"
-	"net/http"
 	"os"
 	"os/signal"
 	"runtime"
 	"syscall"
-	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/http"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
@@ -68,15 +65,10 @@ func Execute(args []string, logWriter io.Writer, version, commit, date string) i
 	openvpnClient := openvpn.NewClient(logger, conf)
 	done := make(chan int, 1)
 
-	server := &http.Server{
-		Addr:              conf.HTTP.Listen,
-		ReadHeaderTimeout: 3 * time.Second,
-		ErrorLog:          slog.NewLogLogger(logger.Handler(), slog.LevelError),
-		Handler:           oauth2.Handler(logger, conf, provider, openvpnClient),
-	}
+	server := http.NewHTTPServer(logger, conf, provider, openvpnClient)
 
 	go func() {
-		if err := startHTTPListener(conf, logger, server); err != nil {
+		if err := server.Listen(); err != nil {
 			logger.Error(fmt.Errorf("error http listener: %w", err).Error())
 			done <- 1
 
@@ -112,15 +104,12 @@ func Execute(args []string, logWriter io.Writer, version, commit, date string) i
 	return returnCode
 }
 
-func shutdown(logger *slog.Logger, openvpnClient *openvpn.Client, server *http.Server) {
+func shutdown(logger *slog.Logger, openvpnClient *openvpn.Client, server http.Server) {
 	openvpnClient.Shutdown()
 
 	logger.Info("start graceful shutdown of http listener")
 
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	if err := server.Shutdown(ctx); err != nil {
+	if err := server.Shutdown(); err != nil {
 		logger.Error(fmt.Errorf("error graceful shutdown: %w", err).Error())
 
 		return
@@ -129,32 +118,6 @@ func shutdown(logger *slog.Logger, openvpnClient *openvpn.Client, server *http.S
 	logger.Info("http listener successfully terminated")
 }
 
-func startHTTPListener(conf config.Config, logger *slog.Logger, server *http.Server) error {
-	if conf.HTTP.TLS {
-		logger.Info(utils.StringConcat(
-			"HTTPS server listen on ", conf.HTTP.Listen, " with base url ", conf.HTTP.BaseURL.String(),
-		))
-
-		err := server.ListenAndServeTLS(conf.HTTP.CertFile, conf.HTTP.KeyFile)
-		if err != nil && !errors.Is(err, http.ErrServerClosed) {
-			return fmt.Errorf("ListenAndServeTLS: %w", err)
-		}
-
-		return nil
-	}
-
-	logger.Info(utils.StringConcat(
-		"HTTP server listen on ", conf.HTTP.Listen, " with base url ", conf.HTTP.BaseURL.String(),
-	))
-
-	err := server.ListenAndServe()
-	if err != nil && !errors.Is(err, http.ErrServerClosed) {
-		return fmt.Errorf("ListenAndServe: %w", err)
-	}
-
-	return nil
-}
-
 func configureLogger(conf config.Config, writer io.Writer) (*slog.Logger, error) {
 	var level slog.Level
 
diff --git a/go.work b/go.work
new file mode 100644
index 0000000..cfb4297
--- /dev/null
+++ b/go.work
@@ -0,0 +1,6 @@
+go 1.21.1
+
+use (
+	.
+	plugin
+)
diff --git a/go.work.sum b/go.work.sum
new file mode 100644
index 0000000..586151b
--- /dev/null
+++ b/go.work.sum
@@ -0,0 +1,20 @@
+cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
+cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
+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/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo=
+github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
+golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
+golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
diff --git a/internal/http/main.go b/internal/http/main.go
new file mode 100644
index 0000000..423d689
--- /dev/null
+++ b/internal/http/main.go
@@ -0,0 +1,74 @@
+package http
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"time"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
+)
+
+type Server struct {
+	conf   config.Config
+	logger *slog.Logger
+	server *http.Server
+}
+
+func NewHTTPServer(logger *slog.Logger, conf config.Config, provider oauth2.Provider, openvpn oauth2.OpenVPN) Server {
+	return Server{
+		conf:   conf,
+		logger: logger,
+		server: &http.Server{
+			Addr:              conf.HTTP.Listen,
+			ReadHeaderTimeout: 3 * time.Second,
+			ErrorLog:          slog.NewLogLogger(logger.Handler(), slog.LevelError),
+			Handler:           oauth2.Handler(logger, conf, provider, openvpn),
+		},
+	}
+}
+
+func (s Server) Listen() error {
+	if s.server == nil {
+		return nil
+	}
+
+	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(),
+		))
+
+		err := s.server.ListenAndServeTLS(s.conf.HTTP.CertFile, s.conf.HTTP.KeyFile)
+		if err != nil && !errors.Is(err, http.ErrServerClosed) {
+			return fmt.Errorf("ListenAndServeTLS: %w", err)
+		}
+
+		return nil
+	}
+
+	s.logger.Info(utils.StringConcat(
+		"start HTTP server listener on ", s.conf.HTTP.Listen, " with base url ", s.conf.HTTP.BaseURL.String(),
+	))
+
+	err := s.server.ListenAndServe()
+	if err != nil && !errors.Is(err, http.ErrServerClosed) {
+		return fmt.Errorf("ListenAndServe: %w", err)
+	}
+
+	return nil
+}
+
+func (s Server) Shutdown() error {
+	if s.server == nil {
+		return nil
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	return s.server.Shutdown(ctx) //nolint:wrapcheck
+}
diff --git a/internal/oauth2/handler.go b/internal/oauth2/handler.go
index 4108f1f..d3e1271 100644
--- a/internal/oauth2/handler.go
+++ b/internal/oauth2/handler.go
@@ -17,13 +17,13 @@ import (
 	"github.com/zitadel/oidc/v2/pkg/oidc"
 )
 
-type openVPN interface {
+type OpenVPN interface {
 	AcceptClient(logger *slog.Logger, client state.ClientIdentifier)
 	AcceptClientWithToken(logger *slog.Logger, client state.ClientIdentifier, username string)
 	DenyClient(logger *slog.Logger, client state.ClientIdentifier, reason string)
 }
 
-func Handler(logger *slog.Logger, conf config.Config, provider Provider, openvpnCallback openVPN) *http.ServeMux {
+func Handler(logger *slog.Logger, conf config.Config, provider Provider, openvpnCallback OpenVPN) *http.ServeMux {
 	basePath := strings.TrimSuffix(conf.HTTP.BaseURL.Path, "/")
 
 	mux := http.NewServeMux()
@@ -34,7 +34,7 @@ func Handler(logger *slog.Logger, conf config.Config, provider Provider, openvpn
 	return mux
 }
 
-func oauth2Start(logger *slog.Logger, provider Provider, conf config.Config, openvpn openVPN) http.Handler {
+func oauth2Start(logger *slog.Logger, provider Provider, conf config.Config, openvpn OpenVPN) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		sessionState := r.URL.Query().Get("state")
 		if sessionState == "" {
@@ -105,7 +105,7 @@ func checkClientIPAddr(r *http.Request, logger *slog.Logger, session state.State
 }
 
 func oauth2Callback(
-	logger *slog.Logger, provider Provider, conf config.Config, openvpn openVPN,
+	logger *slog.Logger, provider Provider, conf config.Config, openvpn OpenVPN,
 ) http.Handler {
 	return rp.CodeExchangeHandler(func(
 		w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], encryptedSession string,
diff --git a/internal/openvpn/client.go b/internal/openvpn/client.go
index 75d6211..455c7f2 100644
--- a/internal/openvpn/client.go
+++ b/internal/openvpn/client.go
@@ -35,19 +35,19 @@ func (c *Client) clientConnect(client connection.Client) error {
 		slog.Uint64("cid", client.Cid),
 		slog.Uint64("kid", client.Kid),
 		slog.String("reason", client.Reason),
-		slog.String("common_name", client.Env["common_name"]),
-		slog.String("username", client.Env["username"]),
+		slog.String("common_name", client.CommonName),
+		slog.String("username", client.Username),
 	)
 
 	logger.Info("new client connection")
 
-	if val, ok := client.Env["common_name"]; ok && slices.Contains(c.conf.OpenVpn.Bypass.CommonNames, val) {
+	if slices.Contains(c.conf.OpenVpn.Bypass.CommonNames, client.CommonName) {
 		logger.Info("client bypass authentication")
 
 		var err error
 
 		if c.conf.OpenVpn.AuthTokenUser {
-			tokenUsername := base64.StdEncoding.EncodeToString([]byte(client.Env["common_name"]))
+			tokenUsername := base64.StdEncoding.EncodeToString([]byte(client.CommonName))
 			_, err = c.SendCommandf("client-auth %d %d\npush \"auth-token-user %s\"\nEND", client.Cid, client.Kid, tokenUsername)
 		} else {
 			_, err = c.SendCommandf("client-auth-nt %d %d", client.Cid, client.Kid)
@@ -69,7 +69,7 @@ func (c *Client) clientConnect(client connection.Client) error {
 		Kid: client.Kid,
 	}
 
-	session := state.New(ClientIdentifier, client.Env["untrusted_ip"], client.Env["common_name"])
+	session := state.New(ClientIdentifier, client.IPAddr, client.CommonName)
 	if err := session.Encode(c.conf.HTTP.Secret); err != nil {
 		return fmt.Errorf("error encoding state: %w", err)
 	}
@@ -93,8 +93,8 @@ func (c *Client) clientDisconnect(client connection.Client) {
 	c.logger.Info("client disconnected",
 		slog.Uint64("cid", client.Cid),
 		slog.String("reason", client.Reason),
-		slog.String("common_name", client.Env["common_name"]),
-		slog.String("username", client.Env["username"]),
+		slog.String("common_name", client.CommonName),
+		slog.String("username", client.Username),
 	)
 }
 
@@ -102,7 +102,7 @@ func (c *Client) clientEstablished(client connection.Client) {
 	c.logger.Info("client established",
 		slog.Uint64("cid", client.Cid),
 		slog.String("reason", client.Reason),
-		slog.String("common_name", client.Env["common_name"]),
-		slog.String("username", client.Env["username"]),
+		slog.String("common_name", client.CommonName),
+		slog.String("username", client.Username),
 	)
 }
diff --git a/internal/openvpn/connection/client.go b/internal/openvpn/connection/client.go
index 0caa9ab..3e416f8 100644
--- a/internal/openvpn/connection/client.go
+++ b/internal/openvpn/connection/client.go
@@ -7,40 +7,50 @@ import (
 )
 
 type Client struct {
-	Kid    uint64
-	Cid    uint64
-	Reason string
-	Env    map[string]string
+	Kid        uint64
+	Cid        uint64
+	Reason     string
+	IPAddr     string
+	CommonName string
+	Username   string
+	IvSSO      string
 }
 
-func NewClient(message string) (Client, error) {
-	clientConnection := Client{
-		Env: map[string]string{},
-	}
+func NewClient(message string) (Client, error) { //nolint:cyclop
+	client := Client{}
 
 	var err error
 
 	for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
 		if isClientReason(line) {
-			clientConnection.Reason, clientConnection.Cid, clientConnection.Kid, err = parseClientReason(line)
+			client.Reason, client.Cid, client.Kid, err = parseClientReason(line)
 			if err != nil {
 				return Client{}, err
 			}
 		} else if strings.HasPrefix(line, ">CLIENT:ENV,") {
 			envKey, envValue := parseClientEnv(line)
-			if envKey == "" {
+			if envKey == "" || envValue == "" {
 				break
 			}
 
-			clientConnection.Env[envKey] = envValue
+			switch envKey {
+			case "untrusted_ip":
+				client.IPAddr = envValue
+			case "common_name":
+				client.CommonName = envValue
+			case "username":
+				client.Username = envValue
+			case "IV_SSO":
+				client.IvSSO = envValue
+			}
 		}
 	}
 
-	if clientConnection.Reason == "" {
+	if client.Reason == "" {
 		return Client{}, fmt.Errorf("unable to parse client reason from message: %s", message)
 	}
 
-	return clientConnection, nil
+	return client, nil
 }
 
 func parseClientEnv(line string) (string, string) {
diff --git a/internal/openvpn/main.go b/internal/openvpn/main.go
index d6c30cc..0ef864f 100644
--- a/internal/openvpn/main.go
+++ b/internal/openvpn/main.go
@@ -148,7 +148,7 @@ func (c *Client) checkManagementInterfaceVersion() error {
 }
 
 func (c *Client) checkClientSsoCapabilities(logger *slog.Logger, client connection.Client) bool {
-	if val, ok := client.Env["IV_SSO"]; ok && strings.Contains(val, "webauth") {
+	if strings.Contains(client.IvSSO, "webauth") {
 		return true
 	}
 
diff --git a/internal/state/state.go b/internal/state/state.go
index 00a385e..2242cff 100644
--- a/internal/state/state.go
+++ b/internal/state/state.go
@@ -18,9 +18,10 @@ type State struct {
 }
 
 type ClientIdentifier struct {
-	Cid      uint64 `json:"c"`
-	Kid      uint64 `json:"k"`
-	Filepath string `json:"fp"`
+	Cid                  uint64 `json:"c"`
+	Kid                  uint64 `json:"k"`
+	AuthFailedReasonFile string `json:"afr"`
+	AuthControlFile      string `json:"ac"`
 }
 
 func New(client ClientIdentifier, ipaddr, commonName string) *State {
diff --git a/plugin/.gitignore b/plugin/.gitignore
new file mode 100644
index 0000000..a959e9e
--- /dev/null
+++ b/plugin/.gitignore
@@ -0,0 +1,3 @@
+config.yaml
+*.h
+*.so
diff --git a/plugin/Dockerfile b/plugin/Dockerfile
new file mode 100644
index 0000000..406b8f0
--- /dev/null
+++ b/plugin/Dockerfile
@@ -0,0 +1,15 @@
+FROM ubuntu:22.04
+
+ENV HOME=/tmp
+
+ARG GO_VERSION=1.21.1
+ARG GO_ARCH=amd64
+
+RUN apt update && apt install --no-install-recommends curl ca-certificates libssl-dev build-essential openvpn -y
+RUN curl -sSfL https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz | tar xzf - -C /usr/local/  \
+    && ln -s /usr/local/go/bin/go /usr/local/bin/go  \
+    && go version
+
+USER 1001
+
+CMD go build -o openvpn-auth-oauth2.so -buildmode=c-shared .
diff --git a/plugin/Makefile b/plugin/Makefile
new file mode 100644
index 0000000..691ece1
--- /dev/null
+++ b/plugin/Makefile
@@ -0,0 +1,13 @@
+.PHONY: builder-image build clean
+
+ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))
+PROJECT_DIR := $(shell dirname $(ROOT_DIR))
+
+clean:
+	@rm -f *.so
+
+build: clean
+	docker run --rm -v $(PROJECT_DIR):/openvpn-auth-oauth2/ -w /openvpn-auth-oauth2/plugin local/openvpn-plugin-build
+
+builder-image:
+	docker build -t local/openvpn-plugin-build .
diff --git a/plugin/callbacks.go b/plugin/callbacks.go
new file mode 100644
index 0000000..2aa2408
--- /dev/null
+++ b/plugin/callbacks.go
@@ -0,0 +1,31 @@
+package main
+
+import "C"
+import (
+	"fmt"
+	"log/slog"
+	"os"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
+)
+
+func (p *PluginHandle) AcceptClient(logger *slog.Logger, client state.ClientIdentifier) {
+	p.writeToAuthFile(logger, client, "1")
+}
+
+func (p *PluginHandle) AcceptClientWithToken(logger *slog.Logger, client state.ClientIdentifier, username string) {
+	p.writeToAuthFile(logger, client, "1")
+}
+
+func (p *PluginHandle) DenyClient(logger *slog.Logger, client state.ClientIdentifier, reason string) {
+	if err := os.WriteFile(client.AuthFailedReasonFile, []byte(reason), 0600); err != nil {
+		logger.Error(fmt.Errorf("write to file %s: %w", client.AuthFailedReasonFile, err).Error())
+	}
+	p.writeToAuthFile(logger, client, "0")
+}
+
+func (p *PluginHandle) writeToAuthFile(logger *slog.Logger, client state.ClientIdentifier, auth string) {
+	if err := os.WriteFile(client.AuthControlFile, []byte(auth), 0600); err != nil {
+		logger.Error(fmt.Errorf("write to file %s: %w", client.AuthFailedReasonFile, err).Error())
+	}
+}
diff --git a/plugin/client.go b/plugin/client.go
new file mode 100644
index 0000000..bb4c316
--- /dev/null
+++ b/plugin/client.go
@@ -0,0 +1,40 @@
+package main
+
+import "C"
+import (
+	"strings"
+	"unsafe"
+)
+
+type Client struct {
+	AuthFailedReasonFile string
+	AuthPendingFile      string
+	AuthControlFile      string
+	IpAddr               string
+	CommonName           string
+}
+
+func NewClient(pointer unsafe.Pointer) Client {
+	envp := (*[1 << 28]*C.char)(pointer)[:128:128]
+	var client Client
+
+	for i := 0; envp[i] != nil; i++ {
+		envParts := strings.SplitN(C.GoString(envp[i]), "=", 2)
+		switch envParts[0] {
+		case "auth_failed_reason_file":
+			client.AuthFailedReasonFile = envParts[1]
+		case "auth_pending_file":
+			client.AuthPendingFile = envParts[1]
+		case "auth_control_file":
+			client.AuthControlFile = envParts[1]
+		case "untrusted_ip":
+			client.IpAddr = envParts[1]
+		case "common_name":
+			client.CommonName = envParts[1]
+		default:
+			continue
+		}
+	}
+
+	return client
+}
diff --git a/plugin/go.mod b/plugin/go.mod
new file mode 100644
index 0000000..8e2ffac
--- /dev/null
+++ b/plugin/go.mod
@@ -0,0 +1,7 @@
+module github.com/jkroepke/openvpn-auth-oauth2/plugin
+
+go 1.21.1
+
+replace github.com/jkroepke/openvpn-auth-oauth2 => ../
+
+require github.com/spf13/pflag v1.0.5 // indirect
diff --git a/plugin/go.sum b/plugin/go.sum
new file mode 100644
index 0000000..287f6fa
--- /dev/null
+++ b/plugin/go.sum
@@ -0,0 +1,2 @@
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
diff --git a/plugin/logger.go b/plugin/logger.go
new file mode 100644
index 0000000..37e3c25
--- /dev/null
+++ b/plugin/logger.go
@@ -0,0 +1,146 @@
+package main
+
+/*
+#include <openvpn-plugin.h>
+static char *MODULE = "openvpn-auth-oauth2";
+
+// A wrapper function is needed because go is not able to call C pointer functions
+// https://stackoverflow.com/questions/37157379/passing-function-pointer-to-the-c-code-using-cgo
+int plugin_log(struct openvpn_plugin_callbacks* cb, int flags, char *msg) {
+	cb->plugin_log(flags, MODULE, "%s", msg);
+	return 0;
+}
+*/
+import "C"
+import (
+	"context"
+	"fmt"
+	"log/slog"
+	"sync"
+	"time"
+	"unsafe"
+)
+
+type PluginHandler struct {
+	opts         Options
+	preformatted []byte
+	mu           *sync.Mutex
+	cb           *C.struct_openvpn_plugin_callbacks
+}
+
+func (h *PluginHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+	if len(attrs) == 0 {
+		return h
+	}
+	h2 := *h
+
+	// Pre-format the attributes.
+	for _, a := range attrs {
+		h2.preformatted = h2.appendAttr(h2.preformatted, a)
+	}
+
+	return &h2
+}
+
+func (h *PluginHandler) WithGroup(name string) slog.Handler {
+	// TODO implement me
+	panic("implement me")
+}
+
+type Options struct {
+	// Level reports the minimum level to log.
+	// Levels with lower levels are discarded.
+	// If nil, the Handler uses [slog.LevelInfo].
+	Level slog.Leveler
+}
+
+func New(cb *C.struct_openvpn_plugin_callbacks, opts *Options) *PluginHandler {
+	h := &PluginHandler{cb: cb, mu: &sync.Mutex{}}
+	if opts != nil {
+		h.opts = *opts
+	}
+	if h.opts.Level == nil {
+		h.opts.Level = slog.LevelInfo
+	}
+
+	return h
+}
+
+func (h *PluginHandler) Enabled(_ context.Context, level slog.Level) bool {
+	return level >= h.opts.Level.Level()
+}
+
+func (h *PluginHandler) Handle(_ context.Context, r slog.Record) error {
+	buf := make([]byte, 0, 1024)
+	buf = fmt.Appendf(buf, "%s: %s", r.Level, r.Message)
+
+	// Insert preformatted attributes just after built-in ones.
+	buf = append(buf, h.preformatted...)
+
+	r.Attrs(func(a slog.Attr) bool {
+		buf = h.appendAttr(buf, a)
+		return true
+	})
+
+	h.mu.Lock()
+	defer h.mu.Unlock()
+
+	msg := C.CString(string(buf))
+	defer C.free(unsafe.Pointer(msg))
+
+	C.plugin_log(h.cb, h.pluginLogLevel(r.Level), msg)
+
+	return nil
+}
+
+func (h *PluginHandler) appendAttr(buf []byte, a slog.Attr) []byte {
+	// Resolve the Attr's value before doing anything else.
+	a.Value = a.Value.Resolve()
+	// Ignore empty Attrs.
+	if a.Equal(slog.Attr{}) {
+		return buf
+	}
+	switch a.Value.Kind() {
+	case slog.KindString:
+		// Quote string values, to make them easy to parse.
+		buf = fmt.Appendf(buf, " %s=%q", a.Key, a.Value.String())
+	case slog.KindTime:
+		// Write times in a standard way, without the monotonic time.
+		buf = fmt.Appendf(buf, " %s=%s", a.Key, a.Value.Time().Format(time.RFC3339Nano))
+	case slog.KindGroup:
+		attrs := a.Value.Group()
+		// Ignore empty groups.
+		if len(attrs) == 0 {
+			return buf
+		}
+		// If the key is non-empty, write it out and indent the rest of the attrs.
+		// Otherwise, inline the attrs.
+		if a.Key != "" {
+			buf = fmt.Appendf(buf, " %s={", a.Key)
+		}
+		for _, ga := range attrs {
+			buf = h.appendAttr(buf, ga)
+		}
+		if a.Key != "" {
+			buf = fmt.Appendf(buf, "}")
+		}
+	default:
+		buf = fmt.Appendf(buf, " %s=%s", a.Key, a.Value)
+	}
+	return buf
+}
+
+func (h *PluginHandler) pluginLogLevel(level slog.Level) C.int {
+	switch level {
+	case slog.LevelError:
+		return C.PLOG_ERR
+	case slog.LevelWarn:
+		return C.PLOG_WARN
+	case slog.LevelDebug:
+		return C.PLOG_DEBUG
+	case slog.LevelInfo:
+		fallthrough
+	default:
+		return C.PLOG_NOTE
+	}
+}
diff --git a/plugin/main.go b/plugin/main.go
new file mode 100644
index 0000000..b619f21
--- /dev/null
+++ b/plugin/main.go
@@ -0,0 +1,28 @@
+package main
+
+import "C"
+import (
+	"log/slog"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/http"
+)
+
+//nolint:gochecknoglobals
+var (
+	version = "dev"
+	commit  = "none"
+	date    = "unknown"
+)
+
+type PluginHandle struct {
+	logger *slog.Logger
+	conf   config.Config
+	server http.Server
+}
+
+func main() {
+	// This function is here to satisfy Go's requirement of having a main function.
+	// The main functionality is implemented in the openvpn_plugin_open_v3 function,
+	// which will be called from C.
+}
diff --git a/plugin/plugin.go b/plugin/plugin.go
new file mode 100644
index 0000000..813a3de
--- /dev/null
+++ b/plugin/plugin.go
@@ -0,0 +1,160 @@
+package main
+
+/*
+#include <openvpn-plugin.h>
+*/
+import "C"
+import (
+	"fmt"
+	"log/slog"
+	"net/url"
+	"os"
+	"slices"
+	"strings"
+	"unsafe"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/http"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
+)
+
+const OPENVPN_PLUGIN_STRUCTVER_MIN = 5
+
+//export openvpn_plugin_select_initialization_point_v1
+func openvpn_plugin_select_initialization_point_v1() C.int {
+	return 2
+}
+
+//export openvpn_plugin_min_version_required_v1
+func openvpn_plugin_min_version_required_v1() C.int {
+	return 3
+}
+
+//export openvpn_plugin_open_v3_go
+func openvpn_plugin_open_v3_go(v3structver C.int, args *C.struct_openvpn_plugin_args_open_in, retptr *C.struct_openvpn_plugin_args_open_return) C.int {
+	if v3structver < OPENVPN_PLUGIN_STRUCTVER_MIN {
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	retptr.type_mask = 1 << C.OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY
+
+	pluginArgs := unsafe.Slice(args.argv, 2)
+
+	logger := slog.New(New(args.callbacks, nil))
+
+	if len(pluginArgs) > 2 {
+		logger.Error("Multiple args declared! One one argument supported!")
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	if len(pluginArgs) < 2 {
+		logger.Error("Missing arguments!")
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	configFile := C.GoString(pluginArgs[1])
+	conf, err := config.Load(config.Plugin, configFile, nil)
+	if err != nil {
+		logger.Error(fmt.Errorf("error loading config: %w", err).Error())
+
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	handle := &PluginHandle{
+		logger: logger,
+		conf:   conf,
+	}
+
+	provider, err := oauth2.NewProvider(logger, conf)
+	if err != nil {
+		logger.Error(err.Error())
+
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	handle.server = http.NewHTTPServer(logger, conf, provider, handle)
+	go func() {
+		if err := handle.server.Listen(); err != nil {
+			logger.Error(fmt.Errorf("error http listener: %w", err).Error())
+			os.Exit(1)
+		}
+	}()
+
+	retptr.handle = (C.openvpn_plugin_handle_t)(unsafe.Pointer(handle))
+
+	logger.Info(fmt.Sprintf("plugin initialization done. version: %s", version))
+	logger.Warn("THIS PLUGIN IS STILL IN EXPERIMENTAL STATE")
+
+	return C.OPENVPN_PLUGIN_FUNC_SUCCESS
+}
+
+//export openvpn_plugin_func_v3_go
+func openvpn_plugin_func_v3_go(v3structver C.int, args *C.struct_openvpn_plugin_args_func_in, _ *C.struct_openvpn_plugin_args_func_return) C.int {
+	if v3structver < OPENVPN_PLUGIN_STRUCTVER_MIN {
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	handle := (*PluginHandle)(unsafe.Pointer(args.handle))
+
+	if args._type != C.OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY {
+		handle.logger.Error("OPENVPN_PLUGIN_? called")
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	client := NewClient(unsafe.Pointer(args.envp))
+
+	if client.CommonName != "" && slices.Contains(handle.conf.OpenVpn.Bypass.CommonNames, client.CommonName) {
+		handle.logger.Info(fmt.Sprintf("client %s bypass authentication", client.CommonName))
+		return C.OPENVPN_PLUGIN_FUNC_SUCCESS
+	}
+
+	clientIdentifier := state.ClientIdentifier{
+		AuthControlFile:      client.AuthControlFile,
+		AuthFailedReasonFile: client.AuthFailedReasonFile,
+	}
+	session := state.New(clientIdentifier, client.IpAddr, client.CommonName)
+	if err := session.Encode(handle.conf.HTTP.Secret); err != nil {
+		handle.logger.Error(fmt.Errorf("encoding state: %w", err).Error())
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	startURL := utils.StringConcat(
+		strings.TrimSuffix(handle.conf.HTTP.BaseURL.String(), "/"),
+		"/oauth2/start?state=", url.QueryEscape(session.Encoded()),
+	)
+
+	pendingAuth := utils.StringConcat("6000\nwebauth\nWEB_AUTH::", startURL)
+
+	if err := os.WriteFile(client.AuthPendingFile, []byte(pendingAuth), 0600); err != nil {
+		handle.logger.Error(fmt.Errorf("write to file %s: %w", client.AuthPendingFile, err).Error())
+		return C.OPENVPN_PLUGIN_FUNC_ERROR
+	}
+
+	return C.OPENVPN_PLUGIN_FUNC_DEFERRED
+}
+
+//export openvpn_plugin_close_v1
+func openvpn_plugin_close_v1(pluginHandle C.openvpn_plugin_handle_t) {
+	handle := (*PluginHandle)(unsafe.Pointer(pluginHandle))
+	if handle == nil {
+		return
+	}
+
+	_ = handle.server.Shutdown()
+
+	handle.logger.Info("plugin closed")
+}
+
+//export openvpn_plugin_abort_v1
+func openvpn_plugin_abort_v1(pluginHandle C.openvpn_plugin_handle_t) {
+	handle := (*PluginHandle)(unsafe.Pointer(pluginHandle))
+	if handle == nil {
+		return
+	}
+
+	_ = handle.server.Shutdown()
+
+	handle.logger.Info("plugin abort")
+}
diff --git a/plugin/plugin_wrapper.go b/plugin/plugin_wrapper.go
new file mode 100644
index 0000000..51cb923
--- /dev/null
+++ b/plugin/plugin_wrapper.go
@@ -0,0 +1,24 @@
+package main
+
+/*
+#cgo CFLAGS: -Wno-discarded-qualifiers -I/usr/include/openvpn/
+#include <openvpn-plugin.h>
+
+int openvpn_plugin_open_v3_go(int version, struct openvpn_plugin_args_open_in *arguments, struct openvpn_plugin_args_open_return *retptr);
+int openvpn_plugin_func_v3_go(int version, struct openvpn_plugin_args_func_in *arguments, struct openvpn_plugin_args_func_return *retptr);
+
+extern int openvpn_plugin_open_v3(const int version,
+    struct openvpn_plugin_args_open_in const *arguments,
+    struct openvpn_plugin_args_open_return *retptr
+) {
+	return openvpn_plugin_open_v3_go(version, arguments, retptr);
+};
+
+extern int openvpn_plugin_func_v3(const int version,
+    struct openvpn_plugin_args_func_in const *arguments,
+    struct openvpn_plugin_args_func_return *retptr
+) {
+	return openvpn_plugin_func_v3_go(version, arguments, retptr);
+};
+*/
+import "C"


Test patch:
diff --git a/internal/openvpn/connection/client_test.go b/internal/openvpn/connection/client_test.go
index 3f7ca20..83387de 100644
--- a/internal/openvpn/connection/client_test.go
+++ b/internal/openvpn/connection/client_test.go
@@ -19,63 +19,55 @@ func TestNewClientConnection(t *testing.T) {
 	}{
 		{
 			"client CONNECT",
-			[]string{">CLIENT:CONNECT,0,1", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
-			connection.Client{Cid: 0, Kid: 1, Reason: "CONNECT", Env: map[string]string{
-				"name1": "val1", "name2": "",
-			}},
+			[]string{
+				">CLIENT:CONNECT,0,1",
+				">CLIENT:ENV,common_name=common_name",
+				">CLIENT:ENV,username=username",
+				">CLIENT:ENV,untrusted_ip=127.0.0.1",
+				">CLIENT:ENV,untrusted_ip=127.0.0.1",
+				">CLIENT:ENV,IV_SSO=webauth",
+				">CLIENT:ENV,END",
+			},
+			connection.Client{
+				Cid: 0, Kid: 1, Reason: "CONNECT", CommonName: "common_name", Username: "username", IPAddr: "127.0.0.1", IvSSO: "webauth",
+			},
 			"",
 		},
 		{
 			"client CONNECT invalid cid",
 			[]string{">CLIENT:CONNECT,k,2", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
-			connection.Client{Cid: 1, Kid: 2, Reason: "CONNECT", Env: map[string]string{
-				"name1": "val1", "name2": "",
-			}},
+			connection.Client{Cid: 1, Kid: 2, Reason: "CONNECT"},
 			"unable to parse cid: strconv.ParseUint: parsing \"k\": invalid syntax",
 		},
 		{
 			"client CONNECT invalid kid",
 			[]string{">CLIENT:CONNECT,1,k", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
-			connection.Client{Cid: 1, Kid: 2, Reason: "CONNECT", Env: map[string]string{
-				"name1": "val1", "name2": "",
-			}},
+			connection.Client{Cid: 1, Kid: 2, Reason: "CONNECT"},
 			"unable to parse kid: strconv.ParseUint: parsing \"k\": invalid syntax",
 		},
 		{
 			"client REAUTH",
-			[]string{">CLIENT:REAUTH,1,2", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
-			connection.Client{Cid: 1, Kid: 2, Reason: "REAUTH", Env: map[string]string{
-				"name1": "val1", "name2": "",
-			}},
+			[]string{">CLIENT:REAUTH,1,2", ">CLIENT:ENV,common_name=common_name", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
+			connection.Client{Cid: 1, Kid: 2, Reason: "REAUTH", CommonName: "common_name"},
 			"",
 		},
 		{
 			"client ESTABLISHED",
-			[]string{">CLIENT:ESTABLISHED,1", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
-			connection.Client{
-				Cid: 1, Kid: 0, Reason: "ESTABLISHED", Env: map[string]string{
-					"name1": "val1", "name2": "",
-				},
-			},
+			[]string{">CLIENT:ESTABLISHED,1", ">CLIENT:ENV,END"},
+			connection.Client{Cid: 1, Kid: 0, Reason: "ESTABLISHED"},
 			"",
 		},
 		{
 			"client DISCONNECT",
 			[]string{">CLIENT:DISCONNECT,1", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
-			connection.Client{
-				Cid: 1, Kid: 0, Reason: "DISCONNECT", Env: map[string]string{
-					"name1": "val1", "name2": "",
-				},
-			},
+			connection.Client{Cid: 1, Kid: 0, Reason: "DISCONNECT"},
 			"",
 		},
 		{
 			"client CR_RESPONSE",
 			[]string{">CLIENT:CR_RESPONSE,1,2,YmFzZTY0", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,END"},
 			connection.Client{
-				Cid: 1, Kid: 2, Reason: "CR_RESPONSE", Env: map[string]string{
-					"name1": "val1",
-				},
+				Cid: 1, Kid: 2, Reason: "CR_RESPONSE",
 			},
 			"",
 		},
diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml
index e0a6eb8..5fc4305 100644
--- a/tests/docker-compose.yaml
+++ b/tests/docker-compose.yaml
@@ -13,9 +13,11 @@ services:
       - "0.0.0.0:1194:1194/udp"
       - "8080:8080/tcp"
       - "8081:8081/tcp"
+      - "9000:9000/tcp"
     volumes:
+      - ./:/tests/:ro
       - ./data:/etc/openvpn
-      - ./openvpn-auth-azure-ad:/usr/local/bin/openvpn-auth-azure-ad
+      - ./../plugin:/plugin/
     restart: always
     environment:
       EASYRSA: /usr/local/share/easy-rsa/easyrsa3
@@ -30,8 +32,6 @@ services:
       - /bin/sh
       - -ec
       - |-
-        chmod +x /usr/local/bin/openvpn-auth-azure-ad
-
         if ! command -v openvpn >/dev/null; then
           apk add --no-cache openvpn openssl
         fi
@@ -70,11 +70,13 @@ services:
         fast-io
         user nobody
         group nogroup
+        auth-gen-token 240 120
 
         management 0.0.0.0 8081 /etc/openvpn/password.txt
-        management-hold
-        management-client-auth
-        auth-gen-token 240 120
+        #management-hold
+        #management-client-auth
+
+        plugin /plugin/openvpn-auth-oauth2.so /plugin/config.yaml
 
         reneg-sec 60
         auth-user-pass-optional
@@ -89,7 +91,7 @@ services:
         resolv-retry infinite
         tls-cert-profile preferred
         persist-tun
-        verb 4
+        verb 9
         reneg-sec 0
         <key>
         $(cat /etc/openvpn/pki/private/${UPN}.key)