Skip to content

Commit 18fc40e

Browse files
authored
feat: Allow custom claim for OpenVPN username (#751)
1 parent 72d437b commit 18fc40e

32 files changed

+881
-354
lines changed

.idea/go.imports.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/openvpn-auth-oauth2/full_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ func TestFull(t *testing.T) {
100100
"--http.listen=" + httpListener.Addr().String(),
101101
"--http.assets-path=../../internal/ui/assets",
102102
"--openvpn.addr=tcp://" + managementInterface.Addr().String(),
103+
"--oauth2.openvpn-username-claim", "sub",
103104
"--oauth2.issuer", resourceServer.URL,
104105
"--oauth2.client.id", clientCredentials.ID,
105106
"--oauth2.client.secret", clientCredentials.Secret.String(),

cmd/openvpn-auth-oauth2/reload_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func TestReload(t *testing.T) {
5757
"--http.listen=" + httpListener.Addr().String(),
5858
"--http.assets-path=../../internal/ui/assets",
5959
"--openvpn.addr=tcp://" + managementInterface.Addr().String(),
60+
"--oauth2.openvpn-username-claim=sub",
6061
"--oauth2.issuer", resourceServer.URL,
6162
"--oauth2.client.id", clientCredentials.ID,
6263
"--oauth2.client.secret", clientCredentials.Secret.String(),

docs/Configuration.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ Usage of openvpn-auth-oauth2:
7878
oauth2 issuer (env: CONFIG_OAUTH2_ISSUER)
7979
--oauth2.nonce
8080
If true, a nonce will be defined on the auth URL which is expected inside the token. (env: CONFIG_OAUTH2_NONCE) (default true)
81+
--oauth2.openvpn-username-cel string
82+
CEL expression to extract the username from the token. The expression must evaluate to a string value. Example: oauth2TokenClaims.sub Note: oauth2.openvpn-username-claim and oauth2.openvpn-username-cel cannot be set at the same time. (env: CONFIG_OAUTH2_OPENVPN__USERNAME__CEL)
83+
--oauth2.openvpn-username-claim string
84+
The claim name in the ID Token which should be used as username in OpenVPN. If empty, the common name is used. (env: CONFIG_OAUTH2_OPENVPN__USERNAME__CLAIM) (default "preferred_username")
8185
--oauth2.pkce
8286
If true, Proof Key for Code Exchange (PKCE) RFC 7636 is used for token exchange. (env: CONFIG_OAUTH2_PKCE) (default true)
8387
--oauth2.provider string
@@ -100,18 +104,18 @@ Usage of openvpn-auth-oauth2:
100104
If true, openvpn-auth-oauth2 uses the OIDC UserInfo endpoint to fetch additional information about the user (e.g. groups). (env: CONFIG_OAUTH2_USER__INFO)
101105
--oauth2.validate.acr value
102106
oauth2 required acr values. Comma separated list. Example: phr,phrh (env: CONFIG_OAUTH2_VALIDATE_ACR)
107+
--oauth2.validate.cel string
108+
CEL expression for custom token validation. The expression must evaluate to a boolean value. Example: openVPNUserCommonName == oauth2TokenClaims.preferred_username (env: CONFIG_OAUTH2_VALIDATE_CEL)
103109
--oauth2.validate.common-name string
104110
validate common_name from OpenVPN with ID Token claim. For example: preferred_username or sub (env: CONFIG_OAUTH2_VALIDATE_COMMON__NAME)
105111
--oauth2.validate.common-name-case-sensitive
106112
If true, openvpn-auth-oauth2 will validate the common case in sensitive mode (env: CONFIG_OAUTH2_VALIDATE_COMMON__NAME__CASE__SENSITIVE)
107113
--oauth2.validate.groups value
108114
oauth2 required user groups. If multiple groups are configured, the user needs to be least in one group. Comma separated list. Example: group1,group2,group3 (env: CONFIG_OAUTH2_VALIDATE_GROUPS)
109-
--oauth2.validate.cel string
110-
CEL expression for custom token validation. The expression must evaluate to a boolean value.
111115
--oauth2.validate.ipaddr
112-
validate client ipaddr between VPN and oidc token (env: CONFIG_OAUTH2_VALIDATE_IPADDR)
116+
validate client ipaddr between VPN and OIDC token (env: CONFIG_OAUTH2_VALIDATE_IPADDR)
113117
--oauth2.validate.issuer
114-
validate issuer from oidc discovery (env: CONFIG_OAUTH2_VALIDATE_ISSUER) (default true)
118+
validate issuer from OIDC discovery (env: CONFIG_OAUTH2_VALIDATE_ISSUER) (default true)
115119
--oauth2.validate.roles value
116120
oauth2 required user roles. If multiple role are configured, the user needs to be least in one role. Comma separated list. Example: role1,role2,role3 (env: CONFIG_OAUTH2_VALIDATE_ROLES)
117121
--openvpn.addr value
@@ -271,3 +275,7 @@ See [Non-interactive session refresh](Non-interactive%20session%20refresh) for m
271275
## Client specific configuration
272276
273277
See [Client specific configuration](Client%20specific%20configuration) for more information.
278+
279+
## OpenVPN username handling
280+
281+
See [OpenVPN Username](OpenVPN%20Username) for more information.

docs/FAQ.md

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,7 @@ A: Read the following documentation to understand the re-authentication behavior
1616
For the Google Provider,
1717
expand the page [Providers](Providers) and look for Google consent screen always asking for permission grant.
1818

19-
## Q: Note Regarding Passing Usernames from OAuth2 Provider to OpenVPN
2019

21-
A: It's important to note that currently,
22-
there is no mechanism to pass the username from the OAuth2 provider back to OpenVPN.
23-
OpenVPN does not offer such an interface at present.
24-
This limitation applies to scenarios where the IP persistence file or statistics may contain empty usernames.
25-
26-
For future enhancements in this area,
27-
we encourage users to up-vote the relevant feature requests on the OpenVPN GitHub repository.
28-
You can find and support these requests at the following link:
29-
[Feature Request on GitHub](https://github.com/OpenVPN/openvpn/issues/299)
30-
31-
## Q: `mismatch: OpenVPNclient is empty` / `username-as-common-name`
32-
33-
A: When setting up `username-as-common-name` on the OpenVPN server, it's crucial to also configure `openvpn.common-name.environment-variable-name` to `username`.
34-
35-
This configuration is indispensable because `username-as-common-name` functions post-authentication. Aligning the environment variable name with `username` guarantees smooth operation.
36-
37-
On authentication, it's expected that common-name is not the values of the username. That may mis-leading, because after authentication, the common name has the correct value at OpenVPN logs.
38-
39-
**Upstream Issue:** [`OpenVPN/openvpn` #498](https://github.com/OpenVPN/openvpn/issues/498#issuecomment-1939194149)
40-
41-
## Q: Options error: No client-side authentication method is specified.
42-
43-
A: Although openvpn-auth-oauth2 theoretically doesn't require client-side authentication, the OpenVPN client expects it.
44-
45-
**Upstream Issue:** [`OpenVPN/openvpn` #501](https://github.com/OpenVPN/openvpn/issues/501) (Please react with :+1: if you're affected.)
46-
47-
**Potential Workarounds:**
48-
49-
1. **Configure Client Certificates**
50-
Implement client certificates to enable client-side authentication.
51-
52-
2. **Use Inline auth-user-pass**
53-
OpenVPN accepts `auth-user-pass` for client-side authentication. You can define the username and password inline to prevent the OpenVPN GUI from requesting a password.
54-
55-
```
56-
<auth-user-pass>
57-
username
58-
password
59-
</auth-user-pass>
60-
```
61-
62-
Note: The username/password can be any dummy value as they won't be validated by openvpn-auth-oauth2 or OpenVPN itself.
6320

6421
## Q: `Provider did not return a id_token. Validation of user data is not possible.` is logged, but my provider is returning an id_token.
6522

docs/OpenVPN Username.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# OpenVPN Username
2+
3+
## Overview
4+
5+
This document covers various aspects of username handling in openvpn-auth-oauth2, including how to pass usernames from OAuth2 providers to OpenVPN, client-side authentication requirements, and configuration options.
6+
7+
## Client-Side Requirements
8+
9+
### Mandatory `auth-user-pass` Configuration
10+
11+
To use username functionality with openvpn-auth-oauth2, the OpenVPN client **must** have `auth-user-pass` configured. This is a mandatory requirement for the authentication flow to work properly.
12+
13+
**Important:** Although openvpn-auth-oauth2 theoretically doesn't require client-side authentication, the OpenVPN client expects it.
14+
15+
You have two options:
16+
17+
1. **Interactive Mode**: Use `auth-user-pass` without credentials, prompting the user for input:
18+
```
19+
auth-user-pass
20+
```
21+
22+
2. **Inline Mode**: Define dummy credentials inline to prevent prompting (recommended for SSO-only authentication):
23+
```
24+
<auth-user-pass>
25+
username
26+
password
27+
</auth-user-pass>
28+
```
29+
30+
Note: The username/password can be any dummy value as they won't be validated by openvpn-auth-oauth2 or OpenVPN itself during the OAuth2 flow.
31+
32+
**Upstream Issue:** [`OpenVPN/openvpn` #501](https://github.com/OpenVPN/openvpn/issues/501) (Please react with :+1: if you're affected.)
33+
34+
### Error: "No client-side authentication method is specified"
35+
36+
If you encounter this error, ensure that `auth-user-pass` is configured in your client configuration as described above.
37+
38+
## Using `username-as-common-name` on OpenVPN Server
39+
40+
When setting up `username-as-common-name` on the OpenVPN server, you **must** also configure `openvpn.common-name.environment-variable-name` to `username`:
41+
42+
```bash
43+
--openvpn.common-name.environment-variable-name=username
44+
```
45+
46+
Or via environment variable:
47+
48+
```dotenv
49+
CONFIG_OPENVPN_COMMON__NAME_ENVIRONMENT__VARIABLE__NAME=username
50+
```
51+
52+
### Why This Configuration Is Required
53+
54+
This configuration is essential because `username-as-common-name` functions **post-authentication**. By aligning the environment variable name with `username`, you ensure smooth operation.
55+
56+
**Important Note:** During authentication, it's expected that the common-name is not the value of the username. This may be misleading because after authentication, the common name has the correct value in OpenVPN logs.
57+
58+
**Upstream Issue:** [`OpenVPN/openvpn` #498](https://github.com/OpenVPN/openvpn/issues/498#issuecomment-1939194149)
59+
60+
## Passing Usernames from OAuth2 Provider to OpenVPN
61+
62+
### Default Behavior
63+
64+
By default, openvpn-auth-oauth2 does not pass the username from the OAuth2 provider to OpenVPN. This limitation is due to OpenVPN's authentication interface design, which does not provide a native mechanism to set the username post-authentication.
65+
66+
**Limitation:** The IP persistence file or statistics in OpenVPN may contain empty usernames when using the default configuration.
67+
68+
**Upstream Issue:** For native OpenVPN support, please up-vote the feature request on GitHub: [`OpenVPN/openvpn` #299](https://github.com/OpenVPN/openvpn/issues/299)
69+
70+
### Using `openvpn.override-username` (Recommended)
71+
72+
**Requires OpenVPN Server 2.7+**
73+
74+
The `openvpn.override-username` configuration option enables passing the username from OAuth2 token claims to OpenVPN using the `override-username` command. This allows real usernames to appear in OpenVPN statistics and logs.
75+
76+
#### Configuration
77+
78+
Enable this feature using:
79+
80+
```bash
81+
--openvpn.override-username
82+
```
83+
84+
Or via environment variable:
85+
86+
```bash
87+
CONFIG_OPENVPN_OVERRIDE__USERNAME=true
88+
```
89+
90+
#### Username Source
91+
92+
The username is extracted from the OAuth2 ID token using one of these configurations (in order of precedence):
93+
94+
1. **`oauth2.openvpn-username-claim`** - Extract username from a specific token claim (default: `preferred_username`)
95+
2. **`oauth2.openvpn-username-cel`** - Use a CEL expression to extract or transform the username from token claims
96+
97+
Example configurations:
98+
99+
```bash
100+
# Use a specific claim
101+
--oauth2.openvpn-username-claim=email
102+
103+
# Use CEL expression for complex transformations
104+
--oauth2.openvpn-username-cel='oauth2TokenClaims.email.split("@")[0]'
105+
```
106+
107+
For more details on CEL expressions, see the [Client token values](Client%20token%20validation.md#cel-language-features) documentation.
108+
109+
#### Important Limitations
110+
111+
⚠️ **OpenVPN Client-Config-Dir Compatibility:**
112+
113+
When `openvpn.override-username` is enabled, OpenVPN's native `client-config-dir` functionality **will not work** because the username is set **after** client configs are read.
114+
115+
**Workaround:** Use openvpn-auth-oauth2's built-in [Client specific configuration](Client%20specific%20configuration.md) feature instead, which:
116+
- Works seamlessly with `openvpn.override-username`
117+
- Uses token claims to lookup configuration files
118+
- Provides additional features like profile selection UI
119+
120+
For more details, see the OpenVPN man page regarding `override-username` limitations.
121+
122+
### Alternative: `openvpn.auth-token-user`
123+
124+
If you're using OpenVPN Server < 2.7 or cannot use `override-username`, the `openvpn.auth-token-user` option provides limited username support:
125+
126+
```bash
127+
--openvpn.auth-token-user
128+
```
129+
130+
This option uses the `auth-token-user` push command to send a base64-encoded username, but only when the client username is empty. This has more limitations compared to `override-username`.

docs/OpenVPN.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#### Working
1313

1414
- OpenVPN 2.6.6 on Linux
15+
- OpenVPN 2.7.0 on Linux
1516

1617
#### Non-Working
1718

@@ -35,3 +36,7 @@
3536

3637
- [network-manager-openvpn-gnome](https://gitlab.gnome.org/GNOME/NetworkManager-openvpn) -
3738
See https://gitlab.gnome.org/GNOME/NetworkManager-openvpn/-/issues/124
39+
40+
## OpenVPN username handling
41+
42+
See [OpenVPN Username](OpenVPN%20Username) for more information.

docs/Providers.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ CONFIG_OAUTH2_PROVIDER=google
126126
CONFIG_OAUTH2_ISSUER=https://accounts.google.com
127127
CONFIG_OAUTH2_CLIENT_ID=162738495-xxxxx.apps.googleusercontent.com
128128
CONFIG_OAUTH2_CLIENT_SECRET=GOCSPX-xxxxxxxx
129+
CONFIG_OAUTH2_OPENVPN_USERNAME_CLAIM=email
129130

130131
# The scopes openid profile email are required, but configured by default.
131132
# https://www.googleapis.com/auth/cloud-identity.groups.readonly is mandatory for group validation.
@@ -144,6 +145,7 @@ oauth2:
144145
client:
145146
id: "162738495-xxxxx.apps.googleusercontent.com"
146147
secret: "GOCSPX-xxxxxxxx"
148+
openvpn-username-claim: "email"
147149
# The scopes openid profile email are required, but configured by default.
148150
# https://www.googleapis.com/auth/cloud-identity.groups.readonly is mandatory for group validation.
149151
# Enabled by default, if scopes aren't set in the config.
@@ -517,3 +519,15 @@ oauth2:
517519
</table>
518520

519521
</details>
522+
523+
## Okta
524+
525+
Contributions for Okta are welcome. Please open an issue, or a pull request if you want to add documentation for Okta.
526+
527+
## Ping Identity
528+
529+
Contributions for Ping Identity are welcome. Please open an issue, or a pull request if you want to add documentation for Ping Identity.
530+
531+
## OneLogin
532+
533+
Contributions for OneLogin are welcome. Please open an issue, or a pull request if you want to add documentation for OneLogin.

docs/Security considerations.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,38 @@
22

33
## Potential Risks Caused by State Reuse
44

5-
There is a potential risk that an attacker could forge state parameters and hijack an OpenVPN session through phishing attacks. To do this, the attacker would need to know both the state encryption key and the OpenVPN session ID. While the encryption key is a static value, the session ID is a randomly generated incrementing number that changes with each new session.
5+
There is a potential risk that an attacker could forge state parameters and hijack an OpenVPN session through phishing attacks. To do this, the attacker would need to know both the state encryption key, and the OpenVPN session ID. While the encryption key is a static value, the session ID is a randomly generated incrementing number that changes with each new session.
66

77
To mitigate this risk, we recommend the following:
88
* **Hardening OpenVPN itself**, for example, by introducing `tls-auth`. This requires the attacker to obtain an additional TLS key.
99
* **Enabling `--http.check.ipaddr`**, which verifies that the IP address of the VPN connection matches that of the HTTP connection.
1010
* **Forcing re-authentication at the SSO provider**, if supported, by setting `--oauth2.authorize-params=prompt=login`. This ensures users must log in again before proceeding.
11+
12+
## Social Engineering Attacks via OIDC Login Links
13+
14+
An attacker could initiate a VPN connection on their own device and then send the generated OIDC login link to an unsuspecting employee via phishing (e.g., email, instant messaging, or SMS). If the employee clicks the link and completes the authentication, the attacker’s VPN session would be authenticated using the employee’s credentials, granting unauthorized access to the network.
15+
16+
### Attack Scenario
17+
18+
1. The attacker initiates an OpenVPN connection from their device
19+
2. openvpn-auth-oauth2 generates an authentication URL for this connection attempt
20+
3. The attacker sends this URL to a target employee (via phishing email, SMS, etc.)
21+
4. The employee clicks the link and authenticates with their credentials
22+
5. The attackers VPN session is now authenticated as the employee, gaining access to the network.
23+
24+
### Mitigations
25+
26+
To protect against this type of social engineering attack, consider implementing the following measures:
27+
28+
* **Enable IP address validation with `--http.check.ipaddr`**: This ensures that the IP address initiating the VPN connection matches the IP address completing the OIDC authentication flow. This is the most effective technical control, as it prevents the attack even if the employee clicks the link, since the authentication will be rejected due to the IP mismatch.
29+
> [!NOTE]
30+
> While `--http.check.ipaddr` provides strong technical protection against this attack vector, it may not be suitable for all environments (e.g., users behind NAT, mobile users with changing IPs, or organizations using forward proxies). In such cases, compensating controls like user education and enhanced monitoring become even more critical.
31+
32+
33+
* **Implement additional authentication context verification**: Consider using OIDC features that provide additional context:
34+
* Use `--oauth2.authorize-params` to request additional claims that can be validated
35+
* Require multifactor authentication (MFA) at the OIDC provider level
36+
37+
* **Session binding improvements**:
38+
* Use short authentication timeouts (`--openvpn.auth-pending-timeout`) to reduce the window of opportunity for attacker’s
39+
* Consider implementing additional session binding mechanisms beyond IP addresses, if your environment supports it.

0 commit comments

Comments
 (0)