Documentation

Server 3.x

APIv4 (Draft)#

NOTE: Work in Progress!

This document describes version 4 of the API provided by eduVPN and Let’s Connect! servers.

The API is intended to be used by the eduVPN, Let’s Connect! and govVPN applications. If you are creating your own application, look here how to register your own client in the server.

Using this document you should be able to implement the API in your VPN client, or provide the same API for your VPN server to leverage the existing VPN clients.

Standards#

We use a simple HTTP API protected by OAuth 2, following all recommendations of the OAuth 2.1 draft specification.

For some further implementation notes and recommendations for the client, please read this document.

Server Discovery#

As there are many servers running eduVPN / Let’s Connect! you need to know which server you need to connect to. This can be either hard-coded in the application, the user can be asked to provide a server address or a “discovery” can be implemented.

For eduVPN specific we implement “server discovery” as documented here.

Server 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 /.well-known/vpn-user-portal, e.g.:

{
    "api": {
        "http://eduvpn.org/api#4": {
            "api_endpoint": "https://vpn.example.org/vpn-user-portal/api/v4",
            "authorization_endpoint": "https://vpn.example.org/vpn-user-portal/oauth/authorize",
            "token_endpoint": "https://vpn.example.org/vpn-user-portal/oauth/token"
        }
    },
    "v": "4.0.0-1.fc41"
}

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

The application MUST retrieve this document at least once per application run, i.e. if the user restarts the application this document MUST be retrieved fresh for each server the client interacts with. An application MAY opt to refresh the document more frequently.

Endpoint Location#

When fetching this document, redirects, e.g. 301, 302, 303, 307 or 308 MUST be followed, but MUST NOT allow redirect to anything else than other https:// URLs, e.g. redirects to http:// MUST be rejected.

Authorization Endpoint#

The authorization_endpoint is used to obtain an authorization code through an “Authorization Request”. The following parameters MUST be set:

When the client’s redirect_uri is the “Loopback Interface Redirection” URL, it MUST add the response_mode parameter with the value form_post, otherwise it can be ignored.

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 for an access and refresh token. The authorization code is obtained through the query parameters added to the redirect_uri, or through the POST body to the redirect_uri. The token endpoint is also used to obtain new access tokens when the current one expires.

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

Using the API#

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:

API Calls#

Profiles#

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#

Request all available VPN profiles:

$ curl \
    -H "Authorization: Bearer abcdefgh" \
    https://vpn.example.org/vpn-user-portal/api/v4/profiles

Response#

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

{
    "Profiles": [
        {
            "Description": {
                "en": "Access to files and printers.",
                "nl": "Toegang tot bestanden en printers."
            },
            "ID": "dfa9f273-bf6e-420e-a6f3-0372e326cf7c",
            "Name": {
                "en": "Employees",
                "nl": "Medewerkers"
            },
            "Priority": 0
        },
        {
            "Description": {
                "en": "Access to network and virtual machine management systems."
            },
            "ID": "dfa9f273-bf6e-420e-a6f3-0372e326cf7c",
            "Name": {
                "en": "Administrators"
            },
            "Priority": 5
        }
    ]
}

The fields are described in the table below, look under the table for additional information.

Key Always Set Type Description Since Deprecated
ID Yes string Profile ID (UUIDv4 in VPN server 4.x) 4.0.0 No
Name Yes object Human readable name(s) for profile 4.0.0 No
Priority Yes int Hint for client to sorting the available profiles, highest number first 4.0.0 No
Description No object Human readable description(s) for profile 4.0.0 No

The ID of type string is a unique identifier for the profile.

The Name is a JSON object of the type map[string]string, i.e. it contains a mapping from language code to (translated) string. It should contain a short (human readable) name of the profile, e.g. “Students”.

The Description is a JSON object of the type map[string]string, i.e. it contains a mapping from language code to (translated) string. It should contain a longer (human readable) description of the profile, e.g. “Get access to the library, file shares and printers.”

The Priority is an unsigned int between 0 and 65535. The highest numeric value has the highest priority. If the values are identical between profiles, the order is undefined.

Connect#

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

Request#

Create a WireGuard configuration file for the “Employees” profile:

$ curl \
    -H "Authorization: Bearer abcdefgh" \
    -H "Accept: application/x-wireguard-profile" \
    --data-urlencode "ID=dfa9f273-bf6e-420e-a6f3-0372e326cf7c" \
    --data-urlencode "PublicKey=nmZ5ExqRpLgJV9yWKlaC7KQ7EAN7eRJ4XBz9eHJPmUU=" \
    "https://vpn.example.org/vpn-user-portal/api/v4/connect"

NOTE: a call to /connect immediately invalidates any previously obtained VPN configuration that belongs to the same OAuth authorization.

The POST request has the following parameters:

Parameter Required Value(s)
ID Yes The ID of profile to retrieve a configuration file for
PublicKey Yes* WireGuard public key

You can influence the VPN configuration profile protocol using the Accept header by adding one or more values to it, separated by a comma. The Accept header MUST be set. The following values are supported:

Accept Header Value Configuration File
application/x-wireguard-profile WireGuard Configuration File
application/x-openvpn-profile OpenVPN Configuration File
application/x-wireguard+proxyguard-profile WireGuard Configuration File with ProxyGuard

If your client does support WireGuard with ProxyGuard, it SHOULD NOT add the application/x-wireguard-profile value to the Accept header to make sure you get a configuration file that contains the ProxyGuard URL in the Endpoint as well.

The value of ID MUST be of one of the identifiers (ID)s for the profiles returned in the /profiles response.

The PublicKey parameter MUST be set if a WireGuard (or WireGuard with ProxyGuard support) configuration is requested. The value of PublicKey MUST be a valid WireGuard public key. It has this format:

$ wg genkey | wg pubkey
e4C2dNBB7k/U8KjS+xZdbicbZsqR1BqWIr1l924P3R4=

You MUST at the very least use a unique key pair for every server and profile combination. You MAY generate a new key pair for every call to /connect. If the key pair is to be stored on-device you SHOULD store it in a protected key store.

If you are requesting an OpenVPN configuration file, the PublicKey key is not needed, all (private) keys will be generated on the server and be part of the returned configuration file.

Response#

You’ll get a WireGuard Configuration File in the wg(8) format, e.g.:

Expires: Wed, 30 Sep 2026 08:44:59 GMT
Content-Type: application/x-wireguard-profile

[Interface]
Address = 10.43.43.2/24,fd43::2/64
DNS = 9.9.9.9,2620:fe::fe

[Peer]
PublicKey = iWAHXts9w9fQVEbA5pVriPlAYMwwEPD5XcVCZDZn1AE=
AllowedIPs = 0.0.0.0/0,::/0
Endpoint = vpn.example:51820

The Expires response header follows the the HTTP Expires header symantics. It indicates until when the VPN configuration can be used on the server.

The API client MUST add the PrivateKey field under [Interface] with the private key that belongs to the public key specified in the /connect request in order to get a full wg(8) compatible configuration file.

The WireGuard Configuration File with ProxyGuard support is identical to the WireGuard Configuration File, except that it modifies the Endpoint key to use URL values to indicate the “protocol”. For example:

Endpoint = vpn.example:51820,https://vpn.example/proxyguard/vpn.example

Disconnect#

This call is to indicate to the server that the VPN session(s) belonging to this OAuth authorization can be terminated.

The purpose of this call is to clean up on the server, e.g. release the IP address reserved for the client.

Request#

$ curl -X POST \
    -H "Authorization: Bearer abcdefgh" \
    "https://vpn.example.org/vpn-user-portal/api/v4/disconnect"

This POST call has no parameters.

Response#

HTTP/1.1 204 No Content

Error Responses#

Call Example Message Code Description
/connect no such "ID" 400 (Bad Request) When the profile does not exist, or the user has no permission
/connect invalid value for "ID" 400 (Bad Request) When the syntax for the ID is invalid
/connect ? 406 (Not Acceptable) When the profile does not support the VPN protocol(s) supported by the client (or vice versa)

An example:

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

{"error":"no such \"ID\""}

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.

Ramblings#

JSON#

Consider supporting also JSON for WireGuard / WireGuard+ProxyGuard configurations, which make it easier for clients to use.

Endpoint#

What would be good for Endpoint values? udp://host:port, proxyguard://host/?

X-Vpn-Gone-Interval#

response header on /connect TODO X-Vpn-Gone-Interval? Time in seconds after which VPN connection is considered “dead” by the server if no handshake occurs within that interval. The value is a 64 bit unsigned integer. Corresponds to App Gone Interval.

The client MUST keep track of the value of X-Vpn-Gone-Interval and check, for example, after resume from suspend, or possibly at other moments like network roaming, whether to interval was exceeded based on the last successful WireGuard handshake.

TODO: add no-cache headers to prevent caching.

TODO: maybe enhance the /profiles response to also include the OAuth session expiry, that way we know when the VPN config is no longer valid. Or would the Expires header be enough?

TODO: can app-gone-interval be deleted? should we simply state that if the WG handshake time was >3 minutes ago, connection check should run again? perhaps triggered automatically after resume?

Priority#

TBD: should we still allow negative numbers? Should we make the lowest value have priority instead of the highest?!

TBD: describe which chars are allowed in which fields, e.g. ID probably uses a limited vocabulary that is “file system” and “URL safe”. The rest, UTF-8… On 4.x server they are UUIDs.

Error Responses#

TODO: other errors could be invalid public key, invalid Accept header, missing parameters, etc.