Secure an api with api keys

Route plugins: Apikeys

Before you start

If you already have an up and running otoroshi instance, you can skip the following instructions

Let’s start by downloading the latest Otoroshi.

curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.15.4/otoroshi.jar'

then you can run start Otoroshi :

java -Dotoroshi.adminPassword=password -jar otoroshi.jar 

Now you can log into Otoroshi at http://otoroshi.oto.tools:8080 with admin@otoroshi.io/password

Create a simple route

From UI

  1. Navigate to http://otoroshi.oto.tools:8080/bo/dashboard/routes and click on the create new route button
  2. Give a name to your route
  3. Save your route
  4. Set myservice.oto.tools as frontend domains
  5. Set https://mirror.otoroshi.io as backend target (hostname: mirror.otoroshi.io, port: 443, Tls: Enabled)

From Admin API

curl -X POST http://otoroshi-api.oto.tools:8080/api/routes \
-H "Content-type: application/json" \
-u admin-api-apikey-id:admin-api-apikey-secret \
-d @- <<'EOF'
{
  "id": "myservice",
  "name": "myapi",
  "frontend": {
    "domains": ["myservice.oto.tools"]
  },
  "backend": {
    "targets": [
      {
        "hostname": "mirror.otoroshi.io",
        "port": 443,
        "tls": true
      }
    ]
  }
}
EOF

Secure routes with api key

By default, a route is public. In our case, we want to secure all paths starting with /api and leave all others unauthenticated.

Let’s add a new plugin, called Apikeys, to our route. Search in the list of plugins, then add it to the flow. Once done, restrict its range by setting up /api in the Informations>include section.

From Admin API

curl -X PUT http://otoroshi-api.oto.tools:8080/api/routes/myservice \
-H "Content-type: application/json" \
-u admin-api-apikey-id:admin-api-apikey-secret \
-d @- <<'EOF'
{
  "id": "myservice",
  "name": "myapi",
  "frontend": {
    "domains": ["myservice.oto.tools"]
  },
  "backend": {
    "targets": [
      {
        "hostname": "mirror.otoroshi.io",
        "port": 443,
        "tls": true
      }
    ]
  },
  "plugins": [
    {
      "enabled": true,
      "plugin": "cp:otoroshi.next.plugins.ApikeyCalls",
      "include": [
          "/api"
      ],
      "config": {
          "validate": true,
          "mandatory": true,
          "wipe_backend_request": true,
          "update_quotas": true
      }
    }
  ]
}
EOF

Navigate to http://myservice.oto.tools:8080/api/test again. If the service is configured, you should have a Service Not found error.

The expected error on the /api/test, indicate that an api key is required to access to this part of the backend service.

Navigate to any other routes which are not starting by /api/* like http://myservice.oto.tools:8080/test/bar

Generate an api key to request secure services

Navigate to http://otoroshi.oto.tools:8080/bo/dashboard/apikeys/add or when clicking on the Add apikey button on the sidebar.

The only required fields of an Otoroshi api key are :

  • ApiKey id
  • ApiKey Secret
  • ApiKey Name

These fields are automatically generated by Otoroshi. However, you can override these values and indicate an additional description.

To simplify the rest of the tutorial, set the values:

  • my-first-api-key-id as ApiKey Id
  • my-first-api-key-secret as ApiKey Secret

Click on Create and stay on this ApiKey button at the bottom of the page.

Now you created the key, it’s time to call our previous generated service with it.

Otoroshi supports 4 methods to achieve that:

First one by passing Otoroshi api key in two headers : Otoroshi-Client-Id and Otoroshi-Client-Secret (these headers names can be override on each service). The second by passing Otoroshi api key in the authentication Header (basically the Authorization header) as a basic encoded value. The third option is to use the bearer generated for your apikey (you can get it by calling curl http://otoroshi-api.oto.tools:8080/api/apikeys/my-first-api-key-id/bearer). A fourth option is to use jwt token but we will not review it here but you can find a tutorial here.

Let’s go ahead and call our service with the first method :

curl -X GET \
  -H 'Otoroshi-Client-Id: my-first-api-key-id' \
  -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \
  'http://myservice.oto.tools:8080/api/test' --include

then with the second method using basic authentication:

curl -X GET \
  -H 'Authorization: Basic bXktZmlyc3QtYXBpLWtleS1pZDpteS1maXJzdC1hcGkta2V5LXNlY3JldA==' \
  'http://myservice.oto.tools:8080/api/test' --include

or

curl -X GET \
  -u my-first-api-key-id:my-first-api-key-secret \
  'http://myservice.oto.tools:8080/api/test' --include

then with the third method using otoroshi bearer:

curl -X GET \
  -H 'Authorization: Bearer otoapk_my-first-api-key-id_99cb8e081d692044593ad0e658a67a5114f7afbdcbeb26f8087cce0df3b610b2' \
  'http://myservice.oto.tools:8080/api/test' --include

Tips : To easily fill your headers, you can jump to the Call examples section in each api key view. In this section the header names are the default values and the service url is not set. You have to adapt these lines to your case.

Override defaults headers names for a route

In some case, we want to change the defaults headers names (and it’s a quite good idea).

Let’s start by navigating to the Apikeys plugin in the Designer of our route.

The first values to change are the headers names used to read the api key from client. Start by clicking on extractors > CustomHeaders and set the following values :

  • api-key-header-id as Custom client id header name
  • api-key-header-secret as Custom client secret header name

Save the route, and call the service again.

curl -X GET \
  -H 'Otoroshi-Client-Id: my-first-api-key-id' \
  -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \
  'http://myservice.oto.tools:8080/api/test' --include

This should output an error because Otoroshi are expecting the api keys in other headers.

{
  "Otoroshi-Error": "No ApiKey provided"
}

Call one again the service but with the changed headers names.

curl -X GET \
  -H 'api-key-header-id: my-first-api-key-id' \
  -H 'api-key-header-secret: my-first-api-key-secret' \
  'http://myservice.oto.tools:8080/api/test' --include

All others default services will continue to accept the api keys with the Otoroshi-Client-Id and Otoroshi-Client-Secret headers, whereas our service, will accept the api-key-header-id and api-key-header-secret headers.

Accept only api keys with expected values

By default, a secure service only accepts requests with api key. But all generated api keys are eligible to call our service and in some case, we want authorize only a couple of api keys.

You can restrict the list of accepted api keys by giving a list of metadata or/and tags. Each api key has a list of tags and metadata, which can be used by Otoroshi to validate a request with an api key. All api key metadata/tags can be forward to your service (see Otoroshi Challenge section of a service to get more information about Otoroshi info. token).

Let’s starting by only accepting api keys with the otoroshi tag.

Click on the ApiKeys plugin, and enabled the Routing section. These constraints guarantee that a request will only be transmitted if all the constraints are validated.

In our first case, set otoroshi in One Tag in array and save the service. Then call our service with :

curl -X GET \
  -H 'Otoroshi-Client-Id: my-first-api-key-id' \
  -H 'Otoroshi-Client-Secret: my-first-api-key-secret' \
  'http://myservice.oto.tools:8080/api/test' --include

This should output :

// Error reason : Our api key doesn't contains the expected tag.
{
  "Otoroshi-Error": "Bad API key"
}

Navigate to the edit page of our api key, and jump to the Metadata and tags section. In this section, add otoroshi in Tags array, then save the api key. Call once again your call and you will normally get a successful response of our backend service.

In this example, we have limited our service to API keys that have otoroshi as a tag.

Otoroshi provides a few others behaviours. For each behaviour, Api key used should:

  • All Tags in : have all of the following tags
  • No Tags in : not have one of the following tags
  • One Tag in : have at least one of the following tags

  • All Meta. in : have all of the following metadata entries
  • No Meta. in : not have one of the following metadata entries
  • One Meta. in : have at least one of the following metadata entries

  • One Meta key in : have at least one of the following key in metadata
  • All Meta key in : have all of the following keys in metadata
  • No Meta key in : not have one of the following keys in metadata