Repository: jkroepke/openvpn-auth-oauth2 (go)

Issue:
validate.common-name fails with "openvpn client is empty"
### Current Behavior

Testing with V1.15.0 on openvpn 2.6.3 (Debian Bookworm):

```
msg="start pending auth" cid=1 kid=1 common_name="" reason=CONNECT username=myself
msg="initialize authorization via oauth2" cid=1 kid=1 common_name=""
msg="deny OpenVPN client cid 1, kid 1" cid=1 kid=1 common_name="" idtoken.preferred_username=MySelf (..) 
msg="user validation: common_name mismatch: openvpn client is empty" cid=1 kid=1 common_name="" (..)
```
My setup is with `username-as-common-name; verify-client-cert none`. When connecting, the management interface will receive
```
>CLIENT:CONNECT,3,1
>CLIENT:ENV,n_clients=0
>CLIENT:ENV,password=password,of_course
>CLIENT:ENV,untrusted_port=53732
>CLIENT:ENV,untrusted_ip=11.22.33.44
>CLIENT:ENV,username=myself
>CLIENT:ENV,IV_SSO=openurl,webauth,crtext
>CLIENT:ENV,IV_GUI_VER=OpenVPN3/Linux/v21
>CLIENT:ENV,IV_CIPHERS=AES-128-GCM:AES-192-GCM:AES-256-GCM:CHACHA20-POLY1305
>CLIENT:ENV,IV_MTU=1600
>CLIENT:ENV,IV_PROTO=990
>CLIENT:ENV,IV_TCPNL=1
>CLIENT:ENV,IV_NCP=2
>CLIENT:ENV,IV_PLAT=linux
>CLIENT:ENV,IV_VER=v3.8.2
>CLIENT:ENV,remote_port_1=1194
>CLIENT:ENV,local_port_1=1194
>CLIENT:ENV,proto_1=udp
>CLIENT:ENV,daemon_pid=44361
>CLIENT:ENV,daemon_start_time=1707741604
>CLIENT:ENV,daemon_log_redirect=1
>CLIENT:ENV,daemon=1
>CLIENT:ENV,verb=4
>CLIENT:ENV,config=/etc/openvpn/server.conf
>CLIENT:ENV,ifconfig_local=192.168.1.1
>CLIENT:ENV,ifconfig_netmask=255.255.255.0
>CLIENT:ENV,script_context=init
>CLIENT:ENV,tun_mtu=1500
>CLIENT:ENV,dev=tun0
>CLIENT:ENV,dev_type=tun
>CLIENT:ENV,redirect_gateway=0
>CLIENT:ENV,END
```
So in case cn is empty (since no certs were used), the common_name should be obtained from CLIENT:ENV,username.
Please compare case-insensitive...

### Expected Behavior

_No response_

### Steps To Reproduce

_No response_

### Environment

_No response_

### openvpn-auth-oauth2 logs

_No response_

### openvpn server logs

_No response_

### Anything else?

_No response_

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


Patch:
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 23ef841..3e6d74e 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -87,6 +87,7 @@ openvpn:
         # - "test"
         # - "test2"
     common-name:
+        environment-variable-name: common_name
         mode: plain
     # password: ""
 provider:
@@ -179,6 +180,8 @@ Usage of openvpn-auth-oauth2:
     	Define auth-token-user for all sessions (env: CONFIG_OPENVPN_AUTH__TOKEN__USER) (default true)
   --openvpn.bypass.common-names value
     	bypass oauth authentication for CNs. Comma separated list. (env: CONFIG_OPENVPN_BYPASS_COMMON__NAMES)
+  --openvpn.common-name.environment-variable string
+    	Name of the environment variable in the OpenVPN management interface which contains the common name. If username-as-common-name is enabled, this should be set to 'username' to use the username as common name. Other values like 'X509_0_emailAddress' are supported. See https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/#environmental-variables for more information. (env: CONFIG_OPENVPN_COMMON__NAME_ENVIRONMENT__VARIABLE) (default "common_name")
   --openvpn.common-name.mode string
     	If common names are too long, use md5/sha1 to hash them or omit to skip them. If omit, oauth2.validate.common-name does not work anymore. Values: [plain,omit] (env: CONFIG_OPENVPN_COMMON__NAME_MODE) (default "plain")
   --openvpn.password value
@@ -318,3 +321,10 @@ CONFIG_OAUTH2_REFRESH_ENABLED=true
 CONFIG_OAUTH2_REFRESH_EXPIRES=8h
 CONFIG_OAUTH2_REFRESH_SECRET= # a static secret to encrypt token. Must be 16, 24 or 32
 ```
+
+## username-as-common-name
+
+If `username-as-common-name` is configured at OpenVPN server,
+ensure that `openvpn.common-name.environment-variable-name` is set to `username` as well.
+
+This is mandatory, because `username-as-common-name` works after the authentication process.
\ No newline at end of file
diff --git a/internal/config/config.go b/internal/config/config.go
index 1bc15e0..b46ecb9 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -139,6 +139,14 @@ func FlagSet(name string) *flag.FlagSet {
 		Defaults.OpenVpn.Bypass.CommonNames,
 		"bypass oauth authentication for CNs. Comma separated list.",
 	)
+	flagSet.String(
+		"openvpn.common-name.environment-variable",
+		Defaults.OpenVpn.CommonName.EnvironmentVariableName,
+		"Name of the environment variable in the OpenVPN management interface which contains the common name. "+
+			"If username-as-common-name is enabled, this should be set to 'username' to use the username as common name. "+
+			"Other values like 'X509_0_emailAddress' are supported. "+
+			"See https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/#environmental-variables for more information.",
+	)
 	flagSet.String(
 		"openvpn.common-name.mode",
 		Defaults.OpenVpn.CommonName.Mode.String(),
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 9a50666..df12c8b 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -39,7 +39,8 @@ var Defaults = Config{
 		AuthTokenUser:      true,
 		AuthPendingTimeout: 3 * time.Minute,
 		CommonName: OpenVPNCommonName{
-			Mode: CommonNameModePlain,
+			EnvironmentVariableName: "common_name",
+			Mode:                    CommonNameModePlain,
 		},
 		Bypass: OpenVpnBypass{
 			CommonNames: make([]string, 0),
diff --git a/internal/config/types.go b/internal/config/types.go
index 1e32855..92dca6a 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -54,7 +54,8 @@ type OpenVpnBypass struct {
 }
 
 type OpenVPNCommonName struct {
-	Mode OpenVPNCommonNameMode `koanf:"mode"`
+	EnvironmentVariableName string                `koanf:"environment-variable-name"`
+	Mode                    OpenVPNCommonNameMode `koanf:"mode"`
 }
 
 type OAuth2 struct {
diff --git a/internal/openvpn/client.go b/internal/openvpn/client.go
index ca92fa4..e68a87f 100644
--- a/internal/openvpn/client.go
+++ b/internal/openvpn/client.go
@@ -36,7 +36,6 @@ func (c *Client) clientConnect(client connection.Client) error {
 		slog.Uint64("kid", client.Kid),
 		slog.String("common_name", client.CommonName),
 		slog.String("reason", client.Reason),
-		slog.String("username", client.Username),
 	)
 
 	logger.Info("new client connection")
@@ -51,7 +50,6 @@ func (c *Client) clientReauth(client connection.Client) error {
 		slog.Uint64("kid", client.Kid),
 		slog.String("common_name", client.CommonName),
 		slog.String("reason", client.Reason),
-		slog.String("username", client.Username),
 	)
 
 	logger.Info("new client reauth")
@@ -134,7 +132,6 @@ func (c *Client) clientEstablished(client connection.Client) {
 		slog.Uint64("cid", client.Cid),
 		slog.String("common_name", client.CommonName),
 		slog.String("reason", client.Reason),
-		slog.String("username", client.Username),
 	)
 }
 
@@ -143,7 +140,6 @@ func (c *Client) clientDisconnect(client connection.Client) {
 		slog.Uint64("cid", client.Cid),
 		slog.String("common_name", client.CommonName),
 		slog.String("reason", client.Reason),
-		slog.String("username", client.Username),
 	)
 
 	logger.Info("client disconnected")
diff --git a/internal/openvpn/connection/client.go b/internal/openvpn/connection/client.go
index 220b37c..cd06fff 100644
--- a/internal/openvpn/connection/client.go
+++ b/internal/openvpn/connection/client.go
@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"strconv"
 	"strings"
+
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 )
 
 type Client struct {
@@ -12,11 +14,10 @@ type Client struct {
 	Reason     string
 	IPAddr     string
 	CommonName string
-	Username   string
 	IvSSO      string
 }
 
-func NewClient(message string) (Client, error) { //nolint:cyclop
+func NewClient(conf config.Config, message string) (Client, error) { //nolint:cyclop
 	client := Client{}
 
 	var (
@@ -47,10 +48,8 @@ func NewClient(message string) (Client, error) { //nolint:cyclop
 				client.IPAddr = envValue
 			case "untrusted_ip6":
 				client.IPAddr = envValue
-			case "common_name":
+			case conf.OpenVpn.CommonName.EnvironmentVariableName:
 				client.CommonName = envValue
-			case "username":
-				client.Username = envValue
 			case "IV_SSO":
 				client.IvSSO = envValue
 			}
diff --git a/internal/openvpn/handler.go b/internal/openvpn/handler.go
index 10ea861..8bdfb6d 100644
--- a/internal/openvpn/handler.go
+++ b/internal/openvpn/handler.go
@@ -96,7 +96,7 @@ func (c *Client) handleMessages() {
 		message = buf.String()
 
 		if strings.HasPrefix(message, ">CLIENT:") {
-			client, err = connection.NewClient(message)
+			client, err = connection.NewClient(c.conf, message)
 			if err != nil {
 				c.errCh <- err
 
diff --git a/packaging/etc/openvpn-auth-oauth2/config.yaml b/packaging/etc/openvpn-auth-oauth2/config.yaml
index ccf4353..5ed8f9f 100644
--- a/packaging/etc/openvpn-auth-oauth2/config.yaml
+++ b/packaging/etc/openvpn-auth-oauth2/config.yaml
@@ -56,6 +56,7 @@
 #        - "test"
 #        - "test2"
 #    common-name:
+#        environment-variable-name: common_name
 #        mode: plain
 #    password: ""
 #provider:


Test patch:
diff --git a/internal/config/load_test.go b/internal/config/load_test.go
index 728b469..6733fda 100644
--- a/internal/config/load_test.go
+++ b/internal/config/load_test.go
@@ -100,6 +100,7 @@ openvpn:
         - "test"
         - "test2"
     common-name:
+        environment-variable-name: X509_0_emailAddress
         mode: omit
     password: "1jd93h5b6s82lf03jh5b2hf9"
 http:
@@ -143,7 +144,8 @@ http:
 					AuthTokenUser:      true,
 					AuthPendingTimeout: 2 * time.Minute,
 					CommonName: config.OpenVPNCommonName{
-						Mode: config.CommonNameModeOmit,
+						EnvironmentVariableName: "X509_0_emailAddress",
+						Mode:                    config.CommonNameModeOmit,
 					},
 				},
 				OAuth2: config.OAuth2{
diff --git a/internal/openvpn/connection/bench_test.go b/internal/openvpn/connection/bench_test.go
index 3293fe0..7c656d8 100644
--- a/internal/openvpn/connection/bench_test.go
+++ b/internal/openvpn/connection/bench_test.go
@@ -3,12 +3,13 @@ package connection_test
 import (
 	"testing"
 
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn/connection"
 )
 
 func BenchmarkConnection(b *testing.B) {
 	for i := 0; i < b.N; i++ {
-		_, _ = connection.NewClient(">CLIENT:CONNECT,0,1\n>CLIENT:ENV,n_clients=0\n>CLIENT:ENV,password=\n>CLIENT:ENV,untrusted_port=17016\n>CLIENT:ENV,untrusted_ip=192.168.65.1\n>CLIENT:ENV,common_name=user@example.com\n>CLIENT:ENV,username=\n>CLIENT:ENV,IV_BS64DL=1\n>CLIENT:ENV,IV_SSO=webauth,openurl,crtext\n>CLIENT:ENV,IV_GUI_VER=OCmacOS_3.4.4-4629\n>CLIENT:ENV,IV_AUTO_SESS=1\n>CLIENT:ENV,IV_CIPHERS=AES-128-GCM:AES-192-GCM:AES-256-GCM:CHACHA20-POLY1305\n>CLIENT:ENV,IV_MTU=1600\n>CLIENT:ENV,IV_PROTO=990\n>CLIENT:ENV,IV_TCPNL=1\n>CLIENT:ENV,IV_NCP=2\n>CLIENT:ENV,IV_PLAT=mac\n>CLIENT:ENV,IV_VER=3.8.1\n>CLIENT:ENV,tls_serial_hex_0=51:b3:55:90:65:af:71:5c:d5:52:2b:0b:00:14:8d:ee\n>CLIENT:ENV,tls_serial_0=108598624241397715647038806614705737198\n>CLIENT:ENV,tls_digest_sha256_0=d3:6d:1d:96:f8:bd:7e:e8:db:c4:0f:53:a1:76:f0:ca:9e:78:63:bf:c6:4a:ac:b9:e6:ed:84:62:f5:ac:5d:b8\n>CLIENT:ENV,tls_digest_0=b7:73:bd:6c:31:31:49:63:0d:0c:11:6d:0c:13:d0:b4:8f:97:33:7d\n>CLIENT:ENV,tls_id_0=CN=user@example.com\n>CLIENT:ENV,X509_0_CN=user@example.com\n>CLIENT:ENV,tls_serial_hex_1=01:b3:95:f8:1a:9f:9f:fe:7c:27:ad:29:c1:93:23:ae:08:7f:ab:36\n>CLIENT:ENV,tls_serial_1=9713888317380397892476539918183380788698917686\n>CLIENT:ENV,tls_digest_sha256_1=75:1a:a1:63:bb:e9:c7:f3:e3:bf:e1:08:f1:36:b7:36:90:04:da:dd:b8:78:b1:cf:d5:ac:09:b6:36:31:a7:db\n>CLIENT:ENV,tls_digest_1=d4:bc:00:89:e5:01:0c:27:3d:ea:4a:b5:42:8b:f7:3d:19:7a:a2:25\n>CLIENT:ENV,tls_id_1=CN=Easy-RSA CA\n>CLIENT:ENV,X509_1_CN=Easy-RSA CA\n>CLIENT:ENV,remote_port_1=1194\n>CLIENT:ENV,local_port_1=1194\n>CLIENT:ENV,proto_1=udp\n>CLIENT:ENV,daemon_pid=7\n>CLIENT:ENV,daemon_start_time=1703401559\n>CLIENT:ENV,daemon_log_redirect=0\n>CLIENT:ENV,daemon=0\n>CLIENT:ENV,verb=3\n>CLIENT:ENV,config=/etc/openvpn/openvpn.conf\n>CLIENT:ENV,ifconfig_local=100.64.0.1\n>CLIENT:ENV,ifconfig_netmask=255.255.255.0\n>CLIENT:ENV,script_context=init\n>CLIENT:ENV,tun_mtu=1500\n>CLIENT:ENV,dev=tun0\n>CLIENT:ENV,dev_type=tun\n>CLIENT:ENV,redirect_gateway=0\n>CLIENT:ENV,END\n")
+		_, _ = connection.NewClient(config.Defaults, ">CLIENT:CONNECT,0,1\n>CLIENT:ENV,n_clients=0\n>CLIENT:ENV,password=\n>CLIENT:ENV,untrusted_port=17016\n>CLIENT:ENV,untrusted_ip=192.168.65.1\n>CLIENT:ENV,common_name=user@example.com\n>CLIENT:ENV,username=\n>CLIENT:ENV,IV_BS64DL=1\n>CLIENT:ENV,IV_SSO=webauth,openurl,crtext\n>CLIENT:ENV,IV_GUI_VER=OCmacOS_3.4.4-4629\n>CLIENT:ENV,IV_AUTO_SESS=1\n>CLIENT:ENV,IV_CIPHERS=AES-128-GCM:AES-192-GCM:AES-256-GCM:CHACHA20-POLY1305\n>CLIENT:ENV,IV_MTU=1600\n>CLIENT:ENV,IV_PROTO=990\n>CLIENT:ENV,IV_TCPNL=1\n>CLIENT:ENV,IV_NCP=2\n>CLIENT:ENV,IV_PLAT=mac\n>CLIENT:ENV,IV_VER=3.8.1\n>CLIENT:ENV,tls_serial_hex_0=51:b3:55:90:65:af:71:5c:d5:52:2b:0b:00:14:8d:ee\n>CLIENT:ENV,tls_serial_0=108598624241397715647038806614705737198\n>CLIENT:ENV,tls_digest_sha256_0=d3:6d:1d:96:f8:bd:7e:e8:db:c4:0f:53:a1:76:f0:ca:9e:78:63:bf:c6:4a:ac:b9:e6:ed:84:62:f5:ac:5d:b8\n>CLIENT:ENV,tls_digest_0=b7:73:bd:6c:31:31:49:63:0d:0c:11:6d:0c:13:d0:b4:8f:97:33:7d\n>CLIENT:ENV,tls_id_0=CN=user@example.com\n>CLIENT:ENV,X509_0_CN=user@example.com\n>CLIENT:ENV,tls_serial_hex_1=01:b3:95:f8:1a:9f:9f:fe:7c:27:ad:29:c1:93:23:ae:08:7f:ab:36\n>CLIENT:ENV,tls_serial_1=9713888317380397892476539918183380788698917686\n>CLIENT:ENV,tls_digest_sha256_1=75:1a:a1:63:bb:e9:c7:f3:e3:bf:e1:08:f1:36:b7:36:90:04:da:dd:b8:78:b1:cf:d5:ac:09:b6:36:31:a7:db\n>CLIENT:ENV,tls_digest_1=d4:bc:00:89:e5:01:0c:27:3d:ea:4a:b5:42:8b:f7:3d:19:7a:a2:25\n>CLIENT:ENV,tls_id_1=CN=Easy-RSA CA\n>CLIENT:ENV,X509_1_CN=Easy-RSA CA\n>CLIENT:ENV,remote_port_1=1194\n>CLIENT:ENV,local_port_1=1194\n>CLIENT:ENV,proto_1=udp\n>CLIENT:ENV,daemon_pid=7\n>CLIENT:ENV,daemon_start_time=1703401559\n>CLIENT:ENV,daemon_log_redirect=0\n>CLIENT:ENV,daemon=0\n>CLIENT:ENV,verb=3\n>CLIENT:ENV,config=/etc/openvpn/openvpn.conf\n>CLIENT:ENV,ifconfig_local=100.64.0.1\n>CLIENT:ENV,ifconfig_netmask=255.255.255.0\n>CLIENT:ENV,script_context=init\n>CLIENT:ENV,tun_mtu=1500\n>CLIENT:ENV,dev=tun0\n>CLIENT:ENV,dev_type=tun\n>CLIENT:ENV,redirect_gateway=0\n>CLIENT:ENV,END\n")
 	}
 	b.ReportAllocs()
 }
diff --git a/internal/openvpn/connection/client_test.go b/internal/openvpn/connection/client_test.go
index d8c89a8..26f2690 100644
--- a/internal/openvpn/connection/client_test.go
+++ b/internal/openvpn/connection/client_test.go
@@ -4,6 +4,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn/connection"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -14,58 +15,102 @@ func TestNewClientConnection(t *testing.T) {
 
 	for _, tt := range []struct {
 		name             string
+		conf             config.Config
 		lines            []string
 		clientConnection connection.Client
 		err              string
 	}{
 		{
 			"client CONNECT",
+			config.Defaults,
 			[]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,IV_SSO=webauth",
+				">CLIENT:ENV,END",
+			},
+			connection.Client{
+				Cid: 0, Kid: 1, Reason: "CONNECT", CommonName: "common_name", IPAddr: "127.0.0.1", IvSSO: "webauth",
+			},
+			"",
+		},
+		{
+			"client CONNECT ipv6",
+			config.Defaults,
+			[]string{
+				">CLIENT:CONNECT,0,1",
+				">CLIENT:ENV,common_name=common_name",
+				">CLIENT:ENV,username=username",
+				">CLIENT:ENV,untrusted_ip6=::1",
+				">CLIENT:ENV,IV_SSO=webauth",
+				">CLIENT:ENV,END",
+			},
+			connection.Client{
+				Cid: 0, Kid: 1, Reason: "CONNECT", CommonName: "common_name", IPAddr: "::1", IvSSO: "webauth",
+			},
+			"",
+		},
+		{
+			"client CONNECT username-as-common-name",
+			(func() config.Config {
+				conf := config.Defaults
+				conf.OpenVpn.CommonName.EnvironmentVariableName = "username"
+
+				return conf
+			})(),
+			[]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,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",
+				Cid: 0, Kid: 1, Reason: "CONNECT", CommonName: "username", IPAddr: "127.0.0.1", IvSSO: "webauth",
 			},
 			"",
 		},
 		{
 			"client CONNECT invalid cid",
+			config.Defaults,
 			[]string{">CLIENT:CONNECT,k,2", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
 			connection.Client{Cid: 1, Kid: 2, Reason: "CONNECT"},
 			"unable to parse cid: strconv.ParseUint: parsing \"k\": invalid syntax",
 		},
 		{
 			"client CONNECT invalid kid",
+			config.Defaults,
 			[]string{">CLIENT:CONNECT,1,k", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
 			connection.Client{Cid: 1, Kid: 2, Reason: "CONNECT"},
 			"unable to parse kid: strconv.ParseUint: parsing \"k\": invalid syntax",
 		},
 		{
 			"client REAUTH",
+			config.Defaults,
 			[]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",
+			config.Defaults,
 			[]string{">CLIENT:ESTABLISHED,1", ">CLIENT:ENV,END"},
 			connection.Client{Cid: 1, Kid: 0, Reason: "ESTABLISHED"},
 			"",
 		},
 		{
 			"client DISCONNECT",
+			config.Defaults,
 			[]string{">CLIENT:DISCONNECT,1", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2=", ">CLIENT:ENV,END"},
 			connection.Client{Cid: 1, Kid: 0, Reason: "DISCONNECT"},
 			"",
 		},
 		{
 			"client CR_RESPONSE",
+			config.Defaults,
 			[]string{">CLIENT:CR_RESPONSE,1,2,YmFzZTY0", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,END"},
 			connection.Client{
 				Cid: 1, Kid: 2, Reason: "CR_RESPONSE",
@@ -74,12 +119,14 @@ func TestNewClientConnection(t *testing.T) {
 		},
 		{
 			"client invalid reason",
+			config.Defaults,
 			[]string{">CLIENT:unknown", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2", ">CLIENT:ENV,END"},
 			connection.Client{},
 			"unable to parse client reason from message: >CLIENT:unknown\n>CLIENT:ENV,name1=val1\n>CLIENT:ENV,name2\n>CLIENT:ENV,END",
 		},
 		{
 			"client invalid reason",
+			config.Defaults,
 			[]string{">CLIENT:CONNECT", ">CLIENT:ENV,name1=val1", ">CLIENT:ENV,name2", ">CLIENT:ENV,END"},
 			connection.Client{},
 			"unable to parse line '>CLIENT:CONNECT': message invalid",
@@ -92,7 +139,7 @@ func TestNewClientConnection(t *testing.T) {
 
 			message := strings.Join(tt.lines, "\n")
 
-			clientConnection, err := connection.NewClient(message)
+			clientConnection, err := connection.NewClient(tt.conf, message)
 			if tt.err == "" {
 				require.NoError(t, err)
 				assert.Equal(t, tt.clientConnection, clientConnection)
diff --git a/internal/openvpn/main_test.go b/internal/openvpn/main_test.go
index 15740cc..148e480 100644
--- a/internal/openvpn/main_test.go
+++ b/internal/openvpn/main_test.go
@@ -63,6 +63,9 @@ func TestClientFull(t *testing.T) {
 					Secret:  testutils.Secret,
 				},
 				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "common_name",
+					},
 					Bypass: config.OpenVpnBypass{CommonNames: make([]string, 0)},
 				},
 			},
@@ -77,6 +80,9 @@ func TestClientFull(t *testing.T) {
 					Secret:  testutils.Secret,
 				},
 				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "common_name",
+					},
 					Bypass:   config.OpenVpnBypass{CommonNames: make([]string, 0)},
 					Password: "password",
 				},
@@ -85,6 +91,23 @@ func TestClientFull(t *testing.T) {
 			"client-pending-auth 1 2 \"WEB_AUTH::",
 			nil,
 		},
+		{
+			name: "with username",
+			conf: config.Config{
+				HTTP: config.HTTP{
+					BaseURL: &url.URL{Scheme: "http", Host: "localhost"},
+					Secret:  testutils.Secret,
+				},
+				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "username",
+					},
+					Bypass: config.OpenVpnBypass{CommonNames: make([]string, 0)},
+				},
+			},
+			client: ">CLIENT:CONNECT,1,2\r\n>CLIENT:ENV,untrusted_ip=127.0.0.1\r\n>CLIENT:ENV,username=test\r\n>CLIENT:ENV,IV_SSO=webauth\r\n>CLIENT:ENV,END\r\n",
+			expect: "client-pending-auth 1 2 \"WEB_AUTH::",
+		},
 		{
 			"with invalid state",
 			config.Config{
@@ -93,6 +116,9 @@ func TestClientFull(t *testing.T) {
 					Secret:  "012345678910111",
 				},
 				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "common_name",
+					},
 					Bypass:   config.OpenVpnBypass{CommonNames: make([]string, 0)},
 					Password: "password",
 				},
@@ -109,6 +135,9 @@ func TestClientFull(t *testing.T) {
 					Secret:  testutils.Secret,
 				},
 				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "common_name",
+					},
 					Bypass:   config.OpenVpnBypass{CommonNames: make([]string, 0)},
 					Password: "password",
 				},
@@ -125,6 +154,9 @@ func TestClientFull(t *testing.T) {
 					Secret:  testutils.Secret,
 				},
 				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "common_name",
+					},
 					Bypass:   config.OpenVpnBypass{CommonNames: []string{"bypass"}},
 					Password: "password",
 				},
@@ -141,6 +173,9 @@ func TestClientFull(t *testing.T) {
 					Secret:  testutils.Secret,
 				},
 				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "common_name",
+					},
 					Bypass:   config.OpenVpnBypass{CommonNames: []string{"bypass"}},
 					Password: "password",
 				},
@@ -157,6 +192,9 @@ func TestClientFull(t *testing.T) {
 					Secret:  testutils.Secret,
 				},
 				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "common_name",
+					},
 					Bypass:   config.OpenVpnBypass{CommonNames: []string{"bypass"}},
 					Password: "password",
 				},
@@ -173,6 +211,9 @@ func TestClientFull(t *testing.T) {
 					Secret:  testutils.Secret,
 				},
 				OpenVpn: config.OpenVpn{
+					CommonName: config.OpenVPNCommonName{
+						EnvironmentVariableName: "common_name",
+					},
 					Bypass:   config.OpenVpnBypass{CommonNames: []string{"bypass"}},
 					Password: "password",
 				},