ref: f2b9d9b07ab253d07eafe94ba1528e862bd92099 vpn-documentation/API_V3.md -rw-r--r-- 18.7 KiB
f2b9d9b0François Kooman update ROADMAP 3 months ago


This document describes the version 3 API provided by a future version of the eduVPN/Let's Connect! servers. The API is intended to be used by the eduVPN and Let's Connect! applications and uses OAuth 2 for authorization.

The API can be used to obtain server information, prepare for a connection and clean up a connection.


The changes made to the API documentation before it is final.

Date Change
2021-08-04 Allow client to specify supported VPN protocols on /info call using the X-Proto-Support HTTP request header
2021-09-01 The vpn_proto field was added to the /info response
The tcp_only POST parameter was added for OpenVPN profiles
The public_key POST parameter is now only required for WireGuard profiles
Remove the X-Proto-Support header again now that we have vpn_proto in /info response
2021-09-02 Add "Error Responses" section
2021-09-20 Restored the default_gateway bool as needed by the NetworkManager client on Linux

#Instance Discovery

This document assumes you already know which server you want to connect to, by its FQDN, e.g. vpn.example.org.

We also provide documentation on how to implement "discovery" for the eduVPN branded application here.


The VPN servers provide an API protected with OAuth 2.1, currently in draft. If the application implemented the APIv2, it will also work as-is with APIv3.

The only difference in the OAuth implementation between APIv2 and APIv3 is that refresh tokens are now single use. When using a refresh token, the response includes also a new refresh token. Should a refresh token be used multiple times, the whole authorization is revoked and the client will need to reauthorize.

After some rudimentary tests, it seems all existing eduVPN/Let's Connect! clients are handling this properly.

#Endpoint Discovery

A "well-known" URL is provided to figure out the OAuth and API endpoint one has to use. The document can be retrieved from /info.json, e.g.:

  "api": {
    "http://eduvpn.org/api#3": {
      "api_endpoint": "https://vpn.example.org/vpn-user-portal/api/v3",
      "authorization_endpoint": "https://vpn.example.org/vpn-user-portal/oauth/authorize",
      "token_endpoint": "https://vpn.example.org/vpn-user-portal/oauth/token"
  "v": "3.0.0-1.fc34"

Servers that provide the http://eduvpn.org/api#3 key under api, support this API.

This file MUST be freshly retrieved before all attempts to connect to a server to make sure any updates to this file are discovered.

#Endpoint Location

Currently we support both /info.json and /.well-known/vpn-user-portal in eduVPN/Let's Connect! 2.x. It would be nice to phase out /info.json.

When fetching this document, redirects, e.g. 301, 302, 303, MUST be followed.

TODO: it MUST follow the redirects, but only for /info.json and /.well-known/vpn-user-portal, not for the endpoints found through it.

TODO: maybe we can "hard code" the list of endpoints as well, so there is no need to advertise them in the /info.json.

#Authorization Endpoint

The authorization_endpoint is used to obtain an authorization code through an "Authorization Request". All query parameters as defined by the OAuth specification are required, even optional ones:

  • client_id;
  • redirect_uri MUST be a support URL as found here;
  • response_type: MUST be code;
  • scope: MUST be config;
  • state;
  • code_challenge_method: MUST be S256;
  • code_challenge.

Please follow the OAuth specification, or use a library for your platform that implements OAuth 2.1.

The authorization_endpoint with its parameters set MUST be opened in the platform's default browser or follow the platform's best practice dealing with application authorization(s). The redirect_uri parameter MUST point back to a location the application can intercept.

All error conditions, both during the authorization phase AND when talking to the API endpoint MUST be handled according to the OAuth specification(s).

#Token Endpoint

The token_endpoint is used to exchange the authorization code, as obtained through the redirect_uri as part of the authorization, for an access and refresh token. It is also used to retrieve new access tokens when the current access token expires.

All error conditions MUST be handled according to the OAuth specification(s).

#Using the API

The API is kept as simple as possible, and a considerable simplification of the APIv2. Every API call below will include a cURL example, and an example response that can be expected.

All POST requests MUST be sent encoded as application/x-www-form-urlencoded.

The API can be used with the access token obtained using the OAuth flow as documented above. The following API calls are available:

  • Get "Info" from the VPN server, including a list of available profiles (/info);
  • "Connect" to a VPN profile (/connect);
  • "Disconnect" from a VPN profile (/disconnect)

#API Calls


This call will show the available VPN profiles for this instance. This will allow the application to show the user which profiles are available.

This GET call has no parameters.


Request all available VPN profiles:

$ curl \
    -H "Authorization: Bearer abcdefgh" \


HTTP/1.1 200 OK
Content-Type: application/json

    "info": {
        "profile_list": [
                "default_gateway": true,
                "display_name": {
                    "en": "Employees",
                    "nl": "Medewerkers"
                "profile_id": "employees",
                "vpn_proto": "openvpn"
                "default_gateway": false,
                "display_name": "Administrators",
                "profile_id": "admins",
                "vpn_proto": "wireguard"

The default_gateway field indicates whether the client is expected to route all traffic over the VPN, or only a subset of it. It is either true or false.

The display_name field can be either of type string or object. When the field is an object, the keys are BCP-47 language codes.

The vpn_proto field indicates whether the VPN profile uses OpenVPN or WireGuard. If you client does not support OpenVPN (or WireGuard) you can filter profiles based on the vpn_proto field and either omit them, or mark them as unsupported.


Get the profile configuration for the profile you want to connect to.


$ curl \
    -d "profile_id=employees" \
    --data-urlencode "public_key=nmZ5ExqRpLgJV9yWKlaC7KQ7EAN7eRJ4XBz9eHJPmUU=" \
    -H "Authorization: Bearer abcdefgh" \

The POST request has (optional) parameters:

Parameter Required? Protocol Value(s)
profile_id Yes All The profile_id as obtained from the /info call
public_key Yes wireguard A WireGuard public key
tcp_only No openvpn Either on or off when specified, defaults to off

The value of profile_id MUST be of one of the profiles returned by the /info call. The value of public_key MUST be a valid WireGuard public key. It has this format:

$ wg genkey | wg pubkey

The tcp_only parameter is used for OpenVPN. The returned configuration file will then only includes remote lines with TCP ports and omit the UDP ports, if any. If no TCP ports are available an error will be returned. The accepted values of the tcp_only parameter are on and off. If you omit tcp_only it is considered to be off, e.g.:

-d "tcp_only=on"

NOTE: do NOT use the same WireGuard private key for different servers, generate one per server.


If the profile is an OpenVPN profile you'll get the complete OpenVPN client configuration with Content-Type: application/x-openvpn-profile, e.g.:

HTTP/1.1 201 Created
Expires: Fri, 06 Aug 2021 03:59:59 GMT
Content-Type: application/x-openvpn-profile

# OpenVPN Client Configuration
dev tun
remote-cert-tls server
verb 3
server-poll-timeout 10
tls-version-min 1.3
data-ciphers AES-256-GCM
reneg-sec 0
# 2048 bit OpenVPN static key
-----BEGIN OpenVPN Static key V1-----
-----END OpenVPN Static key V1-----
remote vpn.example 1194 udp
remote vpn.example 1194 tcp

If the profile is an WireGuard profile you'll get the complete WireGuard client configuration with Content-Type: application/x-wireguard-profile, e.g.:

Expires: Fri, 06 Aug 2021 03:59:59 GMT
Content-Type: application/x-wireguard-profile

Address =, fd00:1234:1234:1234::a0a:a0c/64
DNS =, 2620:fe::fe

PublicKey = Gwcpqv5WeCI3XotETskDXQLfYQk0fi8gEpuCQVIoKGc=
AllowedIPs =, ::/0
Endpoint = vpn.example:51820

You MUST use the Expires response header value to figure out how long the VPN session will be valid for. When implementing the client, make sure you never connect to the VPN server with an expired VPN configuration.

Before using this configuration, your locally generated private key needs to be added under the [Interface] section, e.g.:

PrivateKey = AJmdZTXhNRwMT1CEvXys2T9SNYnXUG2niJVT4biXaX0=



This call is to indicate to the server that the VPN session can be terminated. This MUST ONLY be called when the user decides to stop the VPN connection.

The purpose of this call is to "release" the IP address reserved for the client to make it available for other clients connecting. This is especially important when using a limited IP range for VPN clients.

This call is "best effort", i.e. it is not a big deal when the call fails. No special care has to be taken when this call fails, e.g. the connection is dead, or the application crashes. However, it MUST be called on "application exit" when the user closes the VPN application without disconnecting first, unless the VPN connection can also be managed outside the VPN.

This call MUST be executed after the VPN connection itself has been terminated by the application.


$ curl \
    -d "profile_id=employees" \
    -H "Authorization: Bearer abcdefgh" \

This POST call has 1 parameter, profile_id. Its value MUST be the same as used for the /connect call.


HTTP/1.1 204 No Content

#Error Responses

Call Message Code Description
/connect no TCP connection possible 406 When the specified profile has no OpenVPN processes listening on TCP
/connect profile not available 400 When the profile does not exist, or the user has no permission
/connect invalid "tcp_only" 400 When the specified values are not either on or off
/connect invalid "profile_id" 400 When the syntax for the profile_id is invalid
/disconnect profile not available 400 When the profile does not exist, or the user has no permission
/disconnect invalid "profile_id" 400 When the syntax for the profile_id is invalid

An example:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{"error":"profile not available"}

In addition to these errors, there can also be an error with the server that we did not anticipate or is an unusual situation. In that case the response code will be 500 and the JSON error key will contain more information about the error. This is usually not something the user/client can do anything with and it should probably be shown as a "server error" to the user. Possibly with a "Try Again" button. The exact error response MUST be logged and accessible by the user if so instructed by the support desk, and MAY be shown to the user in full, however a generic "Server Error" could be considered as well, perhaps with a "Details..." button.


Below we describe how the application MUST interact with the API. It does NOT include information on how to handle OAuth. The application MUST properly handle OAuth, including error cases both during the authorization, refreshing tokens and during the use of the API.

  1. Call /info to retrieve a list of available VPN profiles for the user;
  2. Show the available profiles to the user when there is > 1 profile and allow the user to choose;
  3. After the user chose (or there was only 1 profile) perform the /connect call;
  4. Store the configuration file from the response. Make note of the value of the Expires response header to be able to figure out how long your are able to use the VPN configuration;
  5. Connect to the VPN;
  6. Wait for the user to disconnect the VPN...;
  7. Disconnect the VPN;
  8. Call /disconnect.

As long as the configuration is not "expired", according to the Expires response header the same configuration SHOULD be used until the user manually decides to disconnect. This means that during suspend, or temporary unavailable network, the same configuration SHOULD be used. The application SHOULD implement "online detection" to be able to figure out whether the VPN allows any traffic over it or not.

The basic rules:

  1. /connect (and /disconnect) ONLY need to be called when the user decides to connect/disconnect, not when this happens automatically for whatever reason, e.g. suspending the device, network not available, ...;
  2. There are no API calls as long as the VPN is (supposed to be) up (or down).

NOTE if the application implements some kind of "auto connect" on (device or application) start-up that of course MUST call /info and /connect as well! The /info call to be sure the profile is still available (for the user) and the /connect to obtain a configuration.

It can of course happen that the VPN is not working when using the VPN configuration that is not yet expired. In that case the client SHOULD inform the user about this, e.g. through a notification that possibly opens the application if not yet open. This allows the user to (manually) disconnect/connect again restoring the VPN and possibly renewing the authorization when e.g. the authorization was revoked.


  • the default_gateway (bool) field probably needs to be renamed still, maybe to allow_all_traffic or something.
  • talk about limits for the API, for example 1 user can only be online n times;
  • API returns same configuration when client calls /connect multiple times all other things being equal (only WireGuard)?;


  • we should probably rename the /connect call to /setup or /register, or something like this, as there is no actual connection taking place...
  • Clients will have to deal with the scenario that no IP address is available anymore for them, i.e. the /connect call fails
  • Clients will really need a check to verify the VPN connection is up, e.g. ping the remote peer address (gateway?) or simply by checking when the last handshake took place?
  • The certificate/public key will expire exactly at the moment the OAuth refresh and access token no longer work
  • when the computer goes to sleep you can just try to reconnect with the previously obtained configuration, no need to use the API, BUT if connecting doesn't work go back to the API
  • we need a flow diagram...
  • the application SHOULD show a "Renew" button, starting from >= 30 minutes after the OAuth authorization (using the browser) took place. The reason for this 30 minutes is that browser sessions in the portal are valid for 30 minutes in eduVPN/Let's Connect! servers. Renewing before the user is forced to login again results in getting a session that expires at the exact same time as the previous one which is confusing to the user when they are trying out the "Renew" button