This document describes the API provided by the VPN portal. The API can be used by applications wanting to integrate with the VPN software to make it easy for users to configure their VPN.
The API is pragmatic "REST", keeping things as simple as possible without
obsessing about the proper HTTP verb. There are no PUT
and DELETE
requests,
just simple GET
and POST
requests that use "form submit". The responses
are always application/json
though.
OAuth 2.0 is used to provide the API. The following documents are relevant for implementations and should be followed to the letter except when stated differently:
- The OAuth 2.0 Authorization Framework;
- The OAuth 2.0 Authorization Framework: Bearer Token Usage;
- OAuth 2.0 for Native Apps (use latest version of specification);
- Proof Key for Code Exchange by OAuth Public Clients
Implementing OAuth 2.0 correctly is not easy, there are a number of sample applications available for various platforms that can (and probably should) be used as a basis:
A VPN service running at a particular domain is called an instance, e.g.
demo.eduvpn.nl
. An instance can have multiple profiles, e.g.
internet
and office
.
For an application to discover which instances are available to show to the
user a JSON document can be retrieved. For example eduVPN has a document
available at https://static.eduvpn.nl/instances.json
.
The document looks like this:
{
"instances": [
{
"base_uri": "https:\/\/surf.eduvpn.nl\/",
"display_name": "SURF",
"logo_uri": "https:\/\/static.eduvpn.nl\/img\/surfnet.png"
},
...
],
"seq": 25,
"signed_at": "2017-05-11 14:55:03",
"version": 1
}
The base_uri
can be used to perform the API Discovery, see below. The other
fields can be used to enhance the UI for users using the application by
providing a logo and a human readable name for the instance. Users also SHOULD
have the option to provide their own base_uri
in the application UI if their
favorite provider is not listed.
When downloading the instance discovery file, you also MUST fetch the signature
file, which is located in the same folder, but has the .sig
extension, e.g.
https://static.eduvpn.nl/instances.json.sig
.
Using libsodium you can verify the signature using the public key(s) that you hard code in your application. The signature file contains the Base64-encoded signature. See this document for various language bindings.
The flow:
- Download
instances.json
; - Download
instances.json.sig
; - Verify the signature using libsodium and the public key stored in your application
- If you already have a cached version, verify the
seq
field of the new file is higher than theseq
in the cached copy (see Caching section); - Overwrite the cached version if appropriate.
The OAuth and API endpoints can be discovered by requesting a JSON document
from the instance, based on the base_uri
from the "Instance Discovery"
above. This is the content of https://demo.eduvpn.nl/info.json
:
{
"api": {
"http://eduvpn.org/api#1": {
"authorization_endpoint": "https://demo.eduvpn.nl/portal/_oauth/authorize",
"create_config": "https://demo.eduvpn.nl/portal/api.php/create_config",
"profile_list": "https://demo.eduvpn.nl/portal/api.php/profile_list",
"system_messages": "https://demo.eduvpn.nl/portal/api.php/system_messages",
"user_messages": "https://demo.eduvpn.nl/portal/api.php/user_messages"
},
"http://eduvpn.org/api#2": {
"api_base_uri": "https://demo.eduvpn.nl/portal/api.php",
"authorization_endpoint": "https://demo.eduvpn.nl/portal/_oauth/authorize",
"token_endpoint": "https://demo.eduvpn.nl/portal/oauth.php/token"
}
}
}
NOTE: new implementations MUST use http://eduvpn.org/api#2
!
The authorization_endpoint
is then used to obtain an access token by
providing it with the following query parameters, they are all required,
despite some of them being OPTIONAL according to the OAuth specification:
client_id
: the ID that was registered, see below;redirect_uri
; the URL that was registered, see below;response_type
: alwayscode
;scope
: this is alwaysconfig
;state
: a cryptographically secure random string, to avoid CSRF;code_challenge_method
: alwaysS256
;code_challenge
: the code challenge (see RFC 7636).
The authorization_endpoint
URL together with the query parameters is then
opened using a browser, and eventually redirected to the redirect_uri
where
the application can extract the access_token
field from the URL fragment.
The state
parameter is also added to the fragment of the redirect_uri
and
MUST be the same as the state
parameter value of the initial request. The
response also includes expires_in
that indicates when the access token
will expire.
Using the access_token
some additional server information can be obtained,
as well as configurations created. The examples below will use cURL to show
how to use the API.
If the API responds with a 401 it may mean that the user revoked the
application's permission. Permission to use the API needs to be requested again
in that case. The URLs MUST be taken from the info.json
document described
above.
This call will show the available VPN profiles for this instance. This will allow the application to show the user which profiles are available and some basic information, e.g. whether or not two-factor authentication is enabled.
$ curl -H "Authorization: Bearer abcdefgh" \
https://demo.eduvpn.nl/portal/api.php/profile_list
The response looks like this:
{
"profile_list": {
"data": [
{
"display_name": "Internet Access",
"profile_id": "internet",
"two_factor": false
}
],
"ok": true
}
}
API VERSION 2 ONLY
This call will show information about the user, whether or not the user is enrolled for 2FA and whether or not the user is prevented from connecting to the VPN.
$ curl -H "Authorization: Bearer abcdefgh" \
https://demo.eduvpn.nl/portal/api.php/user_info
The response looks like this:
{
"user_info": {
"data": [
{
"two_factor_enrolled": false,
"is_disabled": false
}
],
"ok": true
}
}
A call that can be used to get a full working OpenVPN configuration file
including certificate and key. This MUST NOT be used by "Native Apps". Instead
the separate /create_keypair
and /profile_config
MUST be used as they
allow for obtaining a new configuration without generating a new
certificate/key.
$ curl -H "Authorization: Bearer abcdefgh" \
-d "display_name=eduVPN%20for%20Android&profile_id=internet" \
https://demo.eduvpn.nl/portal/api.php/create_config
This will send a HTTP POST to the API endpoint, /create_config
with the
parameters display_name
and profile_id
to indicate for which profile a
configuration is downloaded.
The acceptable values for profile_id
can be discovered using the
/profile_list
call as shown above.
The response will be an OpenVPN configuration file that can be used "as-is".
API VERSION 2 ONLY
$ curl -H "Authorization: Bearer abcdefgh" \
-d "display_name=eduVPN%20for%20Android" \
https://demo.eduvpn.nl/portal/api.php/create_keypair
This will send a HTTP POST to the API endpoint, /create_keypair
with the
parameter display_name
. It will only create a public and private key and
return them.
{
"create_keypair": {
"data": {
"certificate": "-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----",
"private_key": "-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----"
},
"ok": true
}
}
The certificate and the private key need to be combined with a profile
configuration as <cert>...</cert>
and <key>...</key>
that can be obtained
through the /profile_config
call.
NOTE: a certificate is valid for ALL profiles of a particular instance,
so if an instance has e.g. the profiles internet
and office
, only one
certificate is required!
API VERSION 2 ONLY
Only get the profile configuration without certificate and private key.
$ curl -H "Authorization: Bearer abcdefgh" \
"https://demo.eduvpn.nl/portal/api.php/profile_config?profile_id=internet"
The response will be an OpenVPN configuration file without the <cert>
and
<key>
fields.
$ curl -H "Authorization: Bearer abcdefgh" \
https://demo.eduvpn.nl/portal/api.php/system_messages
The application is able to access the system_messages
endpoint to see if
there are any notifications available. These are the types of messages:
notification
: a plain text message in themessage
field;motd
: a plain text "message of the day" (MotD) of the service, to be displayed to users on login or when establishing a connection to the VPN;maintenance
: an (optional) plain text message in themessage
field and abegin
andend
field with the time stamp;
All message types have the date_time
field indicating the date the message
was created. This can be used as a unique identifier.
The date_time
, begin
and end
fields are in
ISO 8601
format. Note that seconds are also included.
An example:
{
"system_messages": {
"data": [
{
"message": "Hello World!",
"date_time": "2016-12-02T10:42:08Z",
"type": "notification"
}
],
"ok": true
}
}
The messages of type maintenance
will be available through the API until they
are no longer relevant. Messages of type notification
will be always
available through the API until an administrator (manually) removes it.
$ curl -H "Authorization: Bearer abcdefgh" \
https://demo.eduvpn.nl/portal/api.php/user_messages
These are messages specific to the user. It can contain a message about the user being blocked, or other personal messages from the VPN administrator.
These are the types of messages:
notification
: a plain text message in themessage
field;
An example:
{
"user_messages": {
"data": [
{
"message": "Your account has been disabled. Please contact support.",
"date_time": "2016-12-02T10:43:10Z",
"type": "notification"
}
],
"ok": true
}
}
Same considerations apply as for the system_messages
call.
See Application Flow.
In the scenario above, every instance in the discovery file runs their own OAuth server, so for each instance a new token needs to be obtained.
In order to support sharing access tokens between instances, e.g. for guest usage, we introduce three "types" of authorization:
- local: every instance has their own OAuth server;
- federated: there is one central OAuth server, all instances accept tokens from this OAuth server;
- distributed: there is no central OAuth server, tokens from all instances can be used at all (other) instances.
The authorization_type
key indicates which type is used. The supported
values are local
, federated
or distributed
mapping to the three modes
described above.
See API Discovery section above for determining the OAuth endpoints. The application MUST store the obtained access token and bind it to the instance the token was obtained from. If a user wants to use multiple VPN instances, a token MUST be obtained from all of them individually.
Here there is one central OAuth server that MUST be used. The OAuth server is
specified in the discovery file in the authorization_endpoint
and
token_endpoint
keys. When API discovery is performed, the keys for
authorization_endpoint
and token_endpoint
for the specific instance MUST
be ignored. Refreshing access tokens MUST also be done at the central server.
Obtaining an access token from any of the instances listed in the discovery file is enough and can then be used at all the instances. Typically the user has the ability to obtain only an access token at one of the listed instances, because only there they have an account, so the user MUST choose first which of the instances they have an account at.
This is a bit messy from a UX perspective, as the user does not necessarily know for which instance they have an account. In case of eduVPN this will most likely be the instance operated in their institute's home country. So students of the University of Amsterdam will have to choose "The Netherlands" first.
When API discovery is performed, the keys for
authorization_endpoint
and token_endpoint
for the specific instance MUST
be ignored. Refreshing access tokens MUST also be done at the original OAuth
server that was used to obtain the access token.
There are two types of discovery:
- Instance Discovery
- API Discovery
Both are JSON files that can be cached. It MUST be possible for the user to trigger a reload, i.e. get new copies of the cached files. This can be implemented for example using a button, or at the very least, an application restart.
The Instance Discovery files are also signed using public key cryptography, the
signature MUST be verified and the value of the seq
key of the verified file
MUST be >=
the cached copy. It MUST NOT be possible to "rollback", so for the
instances discovery the cached copy MUST be retained.
The API discovery files do not currently have a signature and seq
key, but
MAY in the future.
In API version 2, some calls were added:
POST
to/create_keypair
to create aprivate_key
andcertificate
for an instance to be used with all profiles. This makes it possible to use one key pair per instance;GET
to/profile_config
to obtain only the configuration file, without generating a key pair. This means the configuration can easily be refetched in case an update is needed without creating a new key pair;GET
to/user_info
to obtain user information;- Extended authorization model in "Authorization" section.
For security reasons, API 2 switches to the authorization code flow, together with mitigations described in the following documents:
- OAuth 2.0 for Native Apps (use latest version of specification);
- Proof Key for Code Exchange by OAuth Public Clients
In particular, the use of PKCE is enforced, and the response_type
MUST be
code
for the authorization request.
In version 2, the instances.json
is also signed, so the signature MUST be
validated by the consuming client.