-
-
Notifications
You must be signed in to change notification settings - Fork 55
Client token validation
openvpn-auth-oauth2 supports advanced token validation using the Common Expression Language (CEL). CEL allows you to write custom validation rules to verify that the OAuth2 ID token claims match the OpenVPN user's context.
CEL validation provides a flexible way to enforce security policies by allowing you to write custom expressions that evaluate to true or false. This validation happens:
- During interactive authentication - After the OAuth2 authentication flow completes but before the OpenVPN connection is established
- During token refresh - When an existing OpenVPN session is refreshed using a refresh token (non-interactive authentication)
This ensures that access policies are continuously enforced throughout the lifecycle of the VPN connection, not just during initial authentication.
To enable CEL validation, configure the oauth2.validate.expression property in your configuration file:
oauth2:
validate:
expression: 'openVPNUserCommonName == oauth2TokenClaims.preferred_username'CONFIG_OAUTH2_VALIDATE_EXPRESSION='openVPNUserCommonName == oauth2TokenClaims.preferred_username'Important
CEL validation is performed both during initial OAuth2 authentication and during token refresh. This means your validation rules will be continuously enforced throughout the entire lifecycle of a VPN session. Make sure your expressions account for both scenarios using the authMode variable if needed.
The following variables are available in your CEL expressions:
| Variable | Type | Description |
|---|---|---|
authMode |
string |
The authentication mode: "interactive" (initial OAuth2 login) or "non-interactive" (token refresh) |
openVPNSessionState |
string |
The OpenVPN session state (e.g., "", "Empty", "Initial", "Authenticated", "Expired", "Invalid", "AuthenticatedEmptyUser", "ExpiredEmptyUser") |
openVPNUserCommonName |
string |
The common name (CN) of the OpenVPN client certificate |
openVPNUserIPAddr |
string |
The IP address of the OpenVPN client |
oauth2TokenIPAddr |
string |
The IP address claim from the OAuth2 ID token |
oauth2TokenClaims |
map<string, dynamic> |
All claims from the OAuth2 ID token |
- The CEL expression must evaluate to a boolean (
trueorfalse) - If the expression evaluates to
true, the user is granted access - If the expression evaluates to
false, the user is denied access - If the expression evaluation fails (e.g., syntax error, accessing a non-existent claim), the user is denied access
Use the has() function to safely check for claim existence before accessing it:
oauth2:
validate:
expression: |
has(oauth2TokenClaims.department) &&
oauth2TokenClaims.department == 'engineering'Important
If you try to access a claim that doesn't exist without using has(), the expression evaluation will fail, and the user will be denied access.
Ensure the OpenVPN common name matches the OAuth2 username claim:
oauth2:
validate:
expression: 'openVPNUserCommonName == oauth2TokenClaims.preferred_username'Only allow users with email addresses from specific domains:
oauth2:
validate:
expression: |
has(oauth2TokenClaims.email) &&
oauth2TokenClaims.email.endsWith('@example.com')Combine multiple conditions with logical operators:
oauth2:
validate:
expression: |
openVPNUserCommonName == oauth2TokenClaims.preferred_username &&
has(oauth2TokenClaims.email_verified) &&
oauth2TokenClaims.email_verified == trueAllow access only if the user belongs to specific groups:
oauth2:
validate:
expression: |
has(oauth2TokenClaims.groups) &&
('vpn-users' in oauth2TokenClaims.groups || 'administrators' in oauth2TokenClaims.groups)Validate that the VPN client IP matches the IP address claim from the token:
oauth2:
validate:
expression: 'openVPNUserIPAddr == oauth2TokenIPAddr'Compare usernames in a case-insensitive manner using the lowerAscii() function:
oauth2:
validate:
expression: |
has(oauth2TokenClaims.preferred_username) && openVPNUserCommonName.lowerAscii() == string(oauth2TokenClaims.preferred_username).lowerAscii()Important
When accessing claims from oauth2TokenClaims that you want to use with string functions, you may need to cast them to string using string() since claims are stored as dynamic types.
Combine multiple conditions for sophisticated validation rules:
oauth2:
validate:
expression: |
openVPNUserCommonName == oauth2TokenClaims.sub &&
(
(has(oauth2TokenClaims.role) && oauth2TokenClaims.role == 'admin') ||
(has(oauth2TokenClaims.vpn_access) && oauth2TokenClaims.vpn_access == true)
) &&
(!has(oauth2TokenClaims.account_locked) || oauth2TokenClaims.account_locked == false)Extract and validate the prefix of an email address:
oauth2:
validate:
expression: |
has(oauth2TokenClaims.email) &&
string(oauth2TokenClaims.email).split('@')[0] == openVPNUserCommonNameValidate that a username contains only allowed characters using regular expression:
oauth2:
validate:
expression: |
has(oauth2TokenClaims.preferred_username) &&
string(oauth2TokenClaims.preferred_username).matches('^[a-zA-Z0-9._-]+$')Ensure usernames meet minimum length requirements:
oauth2:
validate:
expression: |
openVPNUserCommonName.size() >= 3 &&
has(oauth2TokenClaims.preferred_username) &&
string(oauth2TokenClaims.preferred_username).size() >= 3Allow different IP ranges based on email domain:
oauth2:
validate:
expression: |
has(oauth2TokenClaims.email) &&
(
(string(oauth2TokenClaims.email).endsWith('@internal.company.com') &&
openVPNUserIPAddr.startsWith('10.0.')) ||
(string(oauth2TokenClaims.email).endsWith('@company.com') &&
openVPNUserIPAddr.startsWith('192.168.'))
)Apply different validation rules based on whether this is an initial login or a token refresh:
oauth2:
validate:
expression: |
authMode == 'interactive' ||
(authMode == 'non-interactive' && has(oauth2TokenClaims.refresh_allowed) && oauth2TokenClaims.refresh_allowed == true)Validate based on the current OpenVPN session state:
oauth2:
validate:
expression: |
openVPNSessionState in ['Initial', 'Authenticated', 'AuthenticatedEmptyUser'] &&
openVPNUserCommonName == oauth2TokenClaims.preferred_usernameCombine authentication mode and session state for fine-grained control:
oauth2:
validate:
expression: |
(authMode == 'interactive' && openVPNSessionState == 'Initial') ||
(authMode == 'non-interactive' && openVPNSessionState == 'Authenticated')This wiki is synced with the docs folder from the code repository! To improve the wiki, create a pull request against the code repository with the suggested changes.