Skip to content

Commit

Permalink
Merge pull request #14 from m-barthelemy/fix/addDevice-and-README
Browse files Browse the repository at this point in the history
Fix/add device and readme
  • Loading branch information
m-barthelemy authored Sep 18, 2021
2 parents b467fe1 + 0d98e89 commit 0feaf6a
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 37 deletions.
68 changes: 41 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
## What is it?
# What is it?

This is quick (and dirty) web application allowing to add a second round of authentication to a Strongswan VPN.
It doesn't replace, and in fact requires, a normal authentication process using passwords or certificates.
This is quick (and dirty) web application allowing to add a second round of authentication to a Strongswan VPN using OAuth2.
It doesn't replace, and in fact requires, the normal Strongswan authentication process using passwords or certificates.

Traditionally, simple IPSec VPN authentication methods involve deploying certificates to the client, or using a login + password.

While IKEv2 permits, in theory, the use of a second round of authentication, default VPN clients installed on OS such as MacOs and Windows have very little compatibility with it.

This project uses the `ext-auth` Strongswan plugin to provide an additional layer of authentication via a user web login and optional 2FA.
This project uses the `ext-auth` Strongswan plugin to hook itself into the authentication flow and provide an additional layer of authentication using OAuth2 (**Google/Gsuite** or **Microsoft Azure**) and optional 2FA.
This can help to protect your organization aganist VPN credentials or certificates being leaked or stolen.

It can also help achieve compliance with some security standards requiring MFA to be implemented for VPNs giving access to sensitive environments.

This tool compatible with all VPN clients and operating systems.


## How does it work?
# How does it work?

- The user registers to the webapp (_before_ connecting to the VPN)
- They authenticate using OAuth2 (for now, Google and Azure Directory are supported)
- Optionally, they are required to complete additional authentication, using an OTP token (independent from the OAuth2 provider 2FA), TouchID/FaceID or a physical security key.
- A "session" is created with the user email, their source IP address and the time when they completed the web authentication
- They now connect to the VPN. Strongswan's `ext-auth` plugin calls this webapp to check if the user has successfully completed a web authentication recently and from the same source IP address. If not, the connection is rejected.

If a user enables this app to send them notifications, they will generally be transparently allowed automatically to connect to the VPN once their VPN session expires, or if they connect from a different location/source IP, as long as their web authentication through this app is valid.
## Transparent OAuth2 auth validation using notifications
If a user enables this app to send them notifications, they will be transparently allowed to connect to the VPN once their VPN session expires, if they connect from a different location/source IP, or if they got disconnected due to network issues, as long as their web authentication through this app is valid.

If they need to sign in again, they will receive a clickable notification taking them to the app, as long as their browser is running. Without a running browser or if they refused to allow notifications from the app, they can still sign in _before_ connecting to the VPN.

## What does it look like?
With Chrome and Firefox, users do **not** need to have this application open in order to transparently (re)validate their VPN web authentication, as long as the browser is running.

# What does it look like?
Home/welcome screen:

<img width="742" alt="Screen Shot 2020-11-24 at 8 37 32 AM" src="https://user-images.githubusercontent.com/2519084/100031318-4d974800-2e30-11eb-95f9-73a143b05b09.png">
Expand All @@ -53,18 +52,21 @@ Successful sign-in:
<img width="615" alt="Screen Shot 2020-12-04 at 9 04 54 AM" src="https://user-images.githubusercontent.com/2519084/101108589-d328ae00-360f-11eb-8367-812057910879.png">


Unsuccessful VPN connection notification, when OAuth2 re-authentication via browser is needed:

<img width="283" alt="Screen Shot 2021-09-18 at 11 41 39 AM" src="https://user-images.githubusercontent.com/2519084/133871478-194d7af6-ee3c-40f5-a332-508d16e20ef7.png">

## Limitations
# Limitations
- The user identity reported by Strongswan **must** match the email reported by the web authentication. However, if the Strongswan identity is the first part of the email address (without @domain.tld), you can modify the `webauth-check.sh` script to add the domain.
- If a user successfully authenticates using this app, someone else on the same local network would be able to reuse the web session, provided they have the user's Strongswan credentials. This by design, since the app matches a web auth with a Strongswan connection only using the Strongswan identity and the source IP address.
- Since the web authentication has to happen before connecting to the VPN, is probably needs to be hosted in a less protected part of your environment.
- Since the web authentication has to happen before connecting to the VPN, this app probably needs to be hosted in a less protected part of your environment.
- There is currently no way to reset a user account if they have lost or changed their 2FA device. However, all you need to do is manually delete the User record in the database (`DELETE FROM users WHERE email='[email protected]'`).
- Strongswan blocks during the call to the `ext-auth` plugin. Since checking the user web authentication against this app is fast, this shouldn't be an issue, unless you have a high number of users connecting almost simultaneously.
- There is currently no limit on how many attempts a user can make at entering a 2FA OTP code or using a Webauthn device.

## Setup
# Setup

### Build
## Build
The easiest way to use this project is to download the precompiled binaries generated with each release at https://github.com/m-barthelemy/vpn-webauth/releases for your system.

Alternatively, you can build the project yourself:
Expand All @@ -74,18 +76,21 @@ go get github.com/m-barthelemy/vpn-webauth

You can also build the provided Dockerfile.

### Deploy


## Run


### Deployment considerations
You probably want to ensure this web app is served over HTTPS: while the OAuth2 flow will be protected by the provider, this app will receive information back from it, and if additional 2FA is required, the code has to be sent to the server.
The app can optionally generate a Let'sEncrypt certificate automatically and use it to provide HTTPS encryption for users (see `SSLMODE` below). However, in a real production deployment scenario, you probably want to have a proxy such as Nginx habndling the SSL/TLS termination.

### Run
If you run the application behind a proxy such as Nginx, you need to make sure that the app receives the **real** user source IP address.
If you run the application behind a proxy such as Nginx, you need to make sure that the app receives the **real** user source IP address.
With Nginx, you can for example add the following directive to your configuration:
```
proxy_set_header X-Forwarded-For $remote_addr;
```
and then set `ORIGINALIPHEADER` to `X-Forwarded-For`.

You should also set a proper database configuration to store your sessions. By default, the app will store them into a Sqlite database in the `/tmp` directory. For a real setup, you can use Mysql or Postgres.
and then set the `ORIGINALIPHEADER` environment variable to `X-Forwarded-For`.

### Strongswan
Make sure that Strongswan was build with the `ext-auth` module. While this is an [official module](https://wiki.strongswan.org/projects/strongswan/wiki/Ext-auth), it is not enabled in all Linux distributions (Ubuntu and Debian don't ship it for example):
Expand Down Expand Up @@ -119,15 +124,24 @@ It expects the following JSON encoded body data:
}
```

## Configuration options

### Database
This app requires a database to store the VPN users, their web sessions and their browser notifications subscriptions.
The database is configured by setting the `DBTYPE` and `DBDSN` environment variables.



# Configuration options
All the configuration parameters have to passed as environment variables.
### Application
## Application
- `CONNECTIONSRETENTION`: how long to keep VPN connections audit logs, in days. Default: `90`.
> NOTE: The connections audit log cleanup task is only run during the application startup. Also, there is currently no way to view this audit log from the app.
- `DBTYPE`: the database engine where the sessions will be stored. Default: `sqlite`. Can be `sqlite`, `postgres`, `mysql`.
- `DBDSN`: the database connection string. Default: `tmp/vpnwa.db`. Check https://gorm.io/docs/connecting_to_the_database.html for examples.
> By default a Sqlite database is created. You probably want to at least change its path. Sqlite is only suitable for testing purposes or for a small number of concurrent users, and will only work with with a single instance of the app. It is recommended to use MySQL or Postgres instead.
Postgres example: `DBDSN="host=127.0.0.1 user=vpnwa password='' database=vpnwa port=5432"`

> NOTE: the app will automatically create the tables and thus needs to have the privileges to do so.
- `ENCRYPTIONKEY`: Key used to encrypt sensitive information in the database. Must be 32 characters. **Mandatory** if `ENFORCEMFA` is set to `true`.
- `EXCLUDEDIDENTITIES`: list of VPN accounts (identities) that do not require any additional authentication by this app, separated by comma. Optional.
Expand All @@ -145,7 +159,7 @@ All the configuration parameters have to passed as environment variables.
> It is recommended that you create and pass your own key.
- `WEBSESSIONVALIDITY`: How long a web authentication is valid. During this time, users don't need to go through the full OAuth2 + MFA process to get a new VPN session since the browser and existing session are considered as trusted. Default: `12h`. Specify custom value as a number and a time unit, for example `48h30m`.

### OAuth2
## OAuth2
- `OAUTH2PROVIDER`: The Oauth2 provider. Can be `google` or `azure`. **Mandatory**.
- `OAUTH2CLIENTID`: Google or Microsoft Client ID. **Mandatory**.
- `OAUTH2CLIENTSECRET`: Google or Microsoft Client Secret. **Mandatory**.
Expand All @@ -155,7 +169,7 @@ All the configuration parameters have to passed as environment variables.
> NOTE: You need to add this app redirect/callback endpoint (`REDIRECTDOMAIN/auth/google/callback` or `REDIRECTDOMAIN/auth/azure/callback`) to the list of allowed callbacks in your Google or Azure credentials configuration console.
### Multi-Factor Authentication
## Multi-Factor Authentication
- `ENFORCEMFA`: Whether to enforce additional 2FA after OAuth2 login. Default: `true`. If enabled, users will have to choose one of the available MFA options (see below).
- `MFAOTP`: Whether to enable OTP token authentication after OAuth2 login. Default: `true`.
- `MFATOUCHID`: Whether to enable Apple TouchID/FaceID and Windows Hello biometrics authentication after OAuth2 login, if a compatible device is detected. Default: `true`.
Expand All @@ -169,14 +183,14 @@ In case a user wants to be able to sign in from multiple browsers or devices, th

It is also possible to sign in from different browsers and devices by using the OTP (authenticator app) feature.

### VPN
## VPN
- `VPNCHECKPASSWORD`: Shared password between the app and the Strongswan `ext-auth` script to protect the endpoint checking for valid user "sessions". Optional.
> If the `/vpn/check` endpoint is publicly available, it is a good idea to set a password to ensure that only your VPN server is allowed to query the app for user sessions. Make sure you also set it in your `ext-auth` configuration.
- `VPNSESSIONVALIDITY`: How long to allow (re)connections to the VPN after completing the web authentication. During this interval the web authentication status is not reverified. Default: `30m`. Specify custom value as a number and a time unit, for example `1h30m`.
> This option aims at reducing the burden put on the users and avoids them having to go through the web auth again if they get disconnected within the configured delay, due for example to poor network connectivity or inactivity.
> NOTE: subsequent VPN connections must come from the same IP address used during the web authentication.
### SSL
## SSL
- `SSLMODE`: whether and how SSL is enabled. Default: `off`. Can be `auto`, `custom`, `proxy`, `off`.
> `off` doesn't enforce SSL at all at the application level. It is only recommended for local testing.
Expand Down
2 changes: 1 addition & 1 deletion models/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (config *Config) Verify() {
}
config.SSLMode = strings.ToLower(config.SSLMode)
if config.SSLMode != "off" && config.SSLMode != "auto" && config.SSLMode != "custom" && config.SSLMode != "proxy" {
log.Fatal("SSLMODE must be one of off, auto, custom, proxy")
log.Fatal("SSLMODE must be one of: off, auto, custom, proxy")
}

}
2 changes: 1 addition & 1 deletion pkged.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion routes/template_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func NewTemplateHandler(config *models.Config) *TemplateHandler {
}

func (g *TemplateHandler) HandleEmbeddedTemplate(response http.ResponseWriter, request *http.Request) {
// Ensure browsers will always user HTTPS, unless running only locally (dev/test mode)
// Ensure browsers will always use HTTPS, unless running only locally (dev/test mode)
if (g.config.Host == "127.0.0.1" || g.config.Host == "localhost") &&
(request.Header.Get(g.config.OriginalProtoHeader) == "https" || g.config.SSLMode != "off") {
response.Header().Set("Strict-Transport-Security", "max-age=31536000; preload")
Expand Down
2 changes: 1 addition & 1 deletion services/user_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (m *UserManager) CheckVpnSession(identity string, ip string) (*models.User,
}
}

sessionResult := m.db.Order("created_at desc").Where("email = ? AND source_ip = ?", identity, ip).First(&session)
sessionResult := m.db.Order("created_at DESC").Where("email = ? AND source_ip = ?", identity, ip).First(&session)
if sessionResult.Error != nil {
if errors.Is(sessionResult.Error, gorm.ErrRecordNotFound) {
return &user, nil, false, nil
Expand Down
8 changes: 3 additions & 5 deletions templates/addDevice.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@
<main>
<center>
<div class="section"></div>
<form action="/auth/validateotp" method="POST">
<div class="container">
<div class="z-depth-1 grey lighten-4 row main-container ">
<div class="z-depth-1 grey lighten-4 row main-container">
<img src="{{.LogoURL}}" class="logo"/>
<br/><br/>
<br/>
<h4 class="section-title">Add new Device or new Browser</h4>
<br/><br/>
<p>You can generate a temporary, one-time code to enter on the new device or browser.</p>
<br/><br/>
<div class="row"">
<div class='col s12'>
<div class="row">
<div class="col s12">
<a id="register-otc" class="btn-large waves-effect blue-grey left mfa-choose-btn">
<i class="large material-icons left" style="font-size: 2.2em;">lock_open</i>&nbsp;Get a single usage code
</a>
Expand All @@ -38,7 +37,6 @@ <h4 class="section-title">Add new Device or new Browser</h4>
</div>
</div>
</div>
</form>
</center>
</main>
</body>
Expand Down
2 changes: 1 addition & 1 deletion templates/assets/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ $(document).ready(async function(){
};
}

// If notifications are enabled and user allowed them, enable either
// If notifications are enabled and the user allowed them, enable either
// Service Worker or SSE.
if (userInfo.EnableNotifications) {
console.log(`Notification.permission=${Notification.permission}`);
Expand Down

0 comments on commit 0feaf6a

Please sign in to comment.