Expression language

The expression language provides an important mechanism for accessing and manipulating Otoroshi data on different inputs. For example, with this mechanism, you can mapping a claim of an inconming token directly in a claim of a generated token (using JWT verifiers). You can add information of the service descriptor traversed such as the domain of the service or the name of the service. This information can be useful on the backend service.

Documentation and examples

 

Date information

${date}the date corresponding to the moment where the request passed. The field is in Date Time format, with default format *yyy-mm-ddTHH:MM:SS.sss*

${date.format('<format>')}same field that the previous but with a chosen format.

Date information

${date}2021-11-25T10:53:25.366+01:00

${date.format('yyy-MM-dd')}2021-11-25

Service information

${service.domain}matched service domain by the query

${service.subdomain}matched service subdomain by the query

${service.tld}matched top level domain by the query

${service.env}Otoroshi environment of the matched service

${service.id}matched service id

${service.name}matched service name

${service.groups[<field>:<default value>]}get nth group of the service or returned the default value

${service.groups[<field>]}get nth group of the service or returned `no-group-<field>` as value

${service.metadata.<field>:<default value>}get the metadata of the service or returned the default value

${service.metadata.<field>}get the metadata of the service or returned `no-meta-<field>` as value

Service information

${service.domain}myservice.oto.tools

${service.subdomain}myservice

${service.tld}oto.tools

${service.env}prod

${service.id}GnzxRRWi...

${service.name}service

${service.groups[0:'unkown group']}unknown-group

${service.groups[0]}default-group

${service.metadata.test:'default-value'}default-value

${service.metadata.foo}bar

Request information

${req.fullUrl}the complete URL with protocol, host and relative URI

${req.path}the path of the request

${req.uri}the relative URI

${req.host}the host of the request

${req.domain}the domain of the request

${req.method}the method of the request

${req.protocol}the protocol of the request

${req.headers.<header>:'<default value>'}get specific header of the request

${req.headers.<header>}get specific header of the request or get `no-header-<header>` as value

${req.query.<query param>:'<default value>'}get specific query param of the request

${req.query.<query param>}get specific query param of the request or get `no-path-param-<path param>` as value

${req.pathparams.<path param>:'<default value>'}get specific path param of the request

${req.pathparams.<path param>}get specific path param of the request or get `no-path-param-<path param>` as value

Request information

${req.fullUrl}http://api.oto.tools:8080/api/?foo=bar

${req.path}/api/

${req.uri}/api/?foo=bar

${req.host}api.oto.tools:8080

${req.domain}api.oto.tools

${req.method}GET

${req.protocol}http

${req.headers.foob:default value>default value

${req.headers.foo}bar

${req.query.foob:default value}default value

${req.query.foo}bar

${req.pathparams.foob:'default value'}default value

${req.pathparams.foo}bar

Apikey information

${apikey.name}if apikey is present, the client name of the apikey

${apikey.id}if apikey is present in the request, the client id of the apikey

${apikey.metadata.<field>:'<default value>'}if apikey is present, got the expected metadata, else got the default value

${apikey.metadata.<field>}if apikey is present, got the expected metadata, else got the default value `no-meta-<field>`

${apikey.tags[<field>:'<default value>']}if apikey is present, got the nth tags or the default value

${apikey.tags[<field>]}if apikey is present, got the nth tags or `no-tag-<field>` as value

Apikey information

${apikey.name}Otoroshi Backoffice ApiKey

${apikey.id}admin-api-apikey-id

${apikey.metadata.myfield:'default value'}default value

${apikey.metadata.foo}bar

${apikey.tags['0':'no-found-tag']}no-found-tag

${apikey.tags['0']}one-tag

Token information

Only on jwt verifier fields

${token.<field>.replace('<a>','<b>')}get token field and replace a value by b or get `no-token-<field>` as value

${token.<field>.replaceAll('<a>','<b>')}get token field and replace **all** a value by b or get `no-token-<field>` as value

${token.<field>|token.<field-2>:<default value>}get claim of the token, ot the second claim of the token or a default value if not present

${token.<field>|token.<field-2>}get claim of the token or `no-token-$field-$field2` if not present

${token.<field>:<default value>}get claim of the token or a default value if not present

${token.<field>}get claim of the token

Token information

${token.foo.replace('o','a')}fao

${token.foo.replaceAll('o','a')}faa

${token.foob|token.foob2:'not-found'}not-found

${token.foob|token.foo}foo

${token.foob:'not-found-foob'}not-found-foob

${token.foo}bar

System Environment information

${env.<field>:<default value>}get system environment variable or a default value if not present

${env.<field>}get system environment variable or `no-env-var-<field>` if not present

System Environment information

${env.java_h:'not-found-java_h'}not-found-java_h

${env.PATH}/usr/local/bin:

Environment information

${config.<field>:<default value>}get environment variable or a default value if not present

${config.<field>}get environment variable or `no-config-<field>` if not present

Environment information

${config.http.ports:'not-found'}not-found

${config.http.port}8080

Context information

${ctx.<field>.replace('<a>','<b>')}get field and replace a value by b in the string, or set `no-ctx-<field>` as value

${ctx.<field>.replaceAll('<a>','<b>')}get field and replace all a value by b in the string, or set `no-ctx-<field>` as value

${ctx.<field>|ctx.<field-2>:<default value>}get field or if empty the second field, or the set the default value

${ctx.<field>|ctx.<field-2>}get field or if empty the second field, or the set `no-ctx-<field>-<field2>` as value

${ctx.<field>:<default value>}get field or the set the default value

${ctx.<field>}get field or the set `no-ctx-<field>` as value

${ctx.useragent.<field>}get user agent field or set `no-ctx-<field>` as value

${ctx.geolocation.<field>}get geolocation field or set `no-ctx-<field>` as value

Context information

${ctx.foo.replace('o','a')}fao

${ctx.foo.replaceAll('o','a')}faa

${ctx.foob|ctx.foot:'not-found'}not-found

${ctx.foob|ctx.foo}bar

${ctx.foob:'other'}other

${ctx.foo}bar

${ctx.useragent.foo}no-ctx-foo

${ctx.geolocation.foo}no-ctx-foo

User information

If call to a private app

${user.name}get user name

${user.email}get user email

${user.metadata.<field>:<default value>}get metadata of user or get the default value

${user.metadata.<field>}get metadata of user or `no-meta-<field>` as value

${user.profile.<field>:<default value>}get field of profile of user or get the default value

${user.profile.<field>} get field of profile of user or `no-profile-<field>` as value

User information

${user.name}Otoroshi Admin

${user.email}admin@otoroshi.io

${user.metadata.username:'not-found'}not-found

${user.metadata.username}no-meta-username

${user.profile.username:'not-found'}not-found

${user.profile.name}Otoroshi Admin

If an input contains a string starting by ${, Otoroshi will try to evaluate the content. If the content doesn’t match a known expression, the ‘bad-expr’ value will be set.

Test the expression language

You can test to get the same values than the right part by creating these following services.

# Let's start by downloading the latest Otoroshi.
curl -L -o otoroshi.jar 'https://github.com/MAIF/otoroshi/releases/download/v16.20.0-dev/otoroshi.jar'

# Once downloading, run Otoroshi.
java -Dotoroshi.adminPassword=password -jar otoroshi.jar 

# Create an authentication module to protect the following route.
curl -X POST http://otoroshi-api.oto.tools:8080/api/auths \
-H "Otoroshi-Client-Id: admin-api-apikey-id" \
-H "Otoroshi-Client-Secret: admin-api-apikey-secret" \
-H 'Content-Type: application/json; charset=utf-8' \
-d @- <<'EOF'
{"type":"basic","id":"auth_mod_in_memory_auth","name":"in-memory-auth","desc":"in-memory-auth","users":[{"name":"User Otoroshi","password":"$2a$10$oIf4JkaOsfiypk5ZK8DKOumiNbb2xHMZUkYkuJyuIqMDYnR/zXj9i","email":"user@foo.bar","metadata":{"username":"roger"},"tags":["foo"],"webauthn":null,"rights":[{"tenant":"*:r","teams":["*:r"]}]}],"sessionCookieValues":{"httpOnly":true,"secure":false}}
EOF


# Create a proxy of the request.otoroshi.io on http://api.oto.tools:8080
curl -X POST http://otoroshi-api.oto.tools:8080/api/routes \
-u admin-api-apikey-id:admin-api-apikey-secret \
-H 'Content-Type: application/json; charset=utf-8' \
-d @- <<'EOF'
{
    "id": "expression-language-api-service",
    "name": "expression-language",
    "enabled": true,
    "frontend": {
        "domains": [
            "api.oto.tools/"
        ]
    },
    "backend": {
        "targets": [
            {
                "hostname": "request.otoroshi.io",
                "port": 443,
                "tls": true
            }
        ]
    },
    "plugins": [
        {
            "enabled": true,
            "plugin": "cp:otoroshi.next.plugins.OverrideHost"
        },
        {
          "enabled": true,
          "plugin": "cp:otoroshi.next.plugins.ApikeyCalls",
          "config": {
              "validate": true,
              "mandatory": true,
              "pass_with_user": true,
              "wipe_backend_request": true,
              "update_quotas": true
          },
          "plugin_index": {
              "validate_access": 1,
              "transform_request": 2,
              "match_route": 0
          }
      },
        {
            "enabled": true,
            "plugin": "cp:otoroshi.next.plugins.AuthModule",
            "config": {
                "pass_with_apikey": true,
                "auth_module": null,
                "module": "auth_mod_in_memory_auth"
            },
            "plugin_index": {
                "validate_access": 1
            }
        },
        {
            "enabled": true,
            "plugin": "cp:otoroshi.next.plugins.AdditionalHeadersIn",
            "config": {
                "headers": {
                    "my-expr-header.apikey.unknown-tag": "${apikey.tags['0':'no-found-tag']}",
                    "my-expr-header.request.uri": "${req.uri}",
                    "my-expr-header.ctx.replace-field-all-value": "${ctx.foo.replaceAll('o','a')}",
                    "my-expr-header.env.unknown-field": "${env.java_h:not-found-java_h}",
                    "my-expr-header.service-id": "${service.id}",
                    "my-expr-header.ctx.unknown-fields": "${ctx.foob|ctx.foot:not-found}",
                    "my-expr-header.apikey.metadata": "${apikey.metadata.foo}",
                    "my-expr-header.request.protocol": "${req.protocol}",
                    "my-expr-header.service-domain": "${service.domain}",
                    "my-expr-header.token.unknown-foo-field": "${token.foob:not-found-foob}",
                    "my-expr-header.service-unknown-group": "${service.groups['0':'unkown group']}",
                    "my-expr-header.env.path": "${env.PATH}",
                    "my-expr-header.request.unknown-header": "${req.headers.foob:default value}",
                    "my-expr-header.service-name": "${service.name}",
                    "my-expr-header.token.foo-field": "${token.foob|token.foo}",
                    "my-expr-header.request.path": "${req.path}",
                    "my-expr-header.ctx.geolocation": "${ctx.geolocation.foo}",
                    "my-expr-header.token.unknown-fields": "${token.foob|token.foob2:not-found}",
                    "my-expr-header.request.unknown-query": "${req.query.foob:default value}",
                    "my-expr-header.service-subdomain": "${service.subdomain}",
                    "my-expr-header.date": "${date}",
                    "my-expr-header.ctx.replace-field-value": "${ctx.foo.replace('o','a')}",
                    "my-expr-header.apikey.name": "${apikey.name}",
                    "my-expr-header.request.full-url": "${req.fullUrl}",
                    "my-expr-header.ctx.default-value": "${ctx.foob:other}",
                    "my-expr-header.service-tld": "${service.tld}",
                    "my-expr-header.service-metadata": "${service.metadata.foo}",
                    "my-expr-header.ctx.useragent": "${ctx.useragent.foo}",
                    "my-expr-header.service-env": "${service.env}",
                    "my-expr-header.request.host": "${req.host}",
                    "my-expr-header.config.unknown-port-field": "${config.http.ports:not-found}",
                    "my-expr-header.request.domain": "${req.domain}",
                    "my-expr-header.token.replace-header-value": "${token.foo.replace('o','a')}",
                    "my-expr-header.service-group": "${service.groups['0']}",
                    "my-expr-header.ctx.foo": "${ctx.foo}",
                    "my-expr-header.apikey.tag": "${apikey.tags['0']}",
                    "my-expr-header.service-unknown-metadata": "${service.metadata.test:default-value}",
                    "my-expr-header.apikey.id": "${apikey.id}",
                    "my-expr-header.request.header": "${req.headers.foo}",
                    "my-expr-header.request.method": "${req.method}",
                    "my-expr-header.ctx.foo-field": "${ctx.foob|ctx.foo}",
                    "my-expr-header.config.port": "${config.http.port}",
                    "my-expr-header.token.unknown-foo": "${token.foo}",
                    "my-expr-header.date-with-format": "${date.format('yyy-MM-dd')}",
                    "my-expr-header.apikey.unknown-metadata": "${apikey.metadata.myfield:default value}",
                    "my-expr-header.request.query": "${req.query.foo}",
                    "my-expr-header.token.replace-header-all-value": "${token.foo.replaceAll('o','a')}"
                }
            }
        }
    ]
}
EOF

Create an apikey or use the default generate apikey.

curl -X POST 'http://otoroshi-api.oto.tools:8080/api/apikeys' \
-H "Content-type: application/json" \
-u admin-api-apikey-id:admin-api-apikey-secret \
-d @- <<'EOF'
{
    "clientId": "api-apikey-id",
    "clientSecret": "api-apikey-secret",
    "clientName": "api-apikey-name",
    "description": "api-apikey-id-description",
    "authorizedGroup": "default",
    "enabled": true,
    "throttlingQuota": 10,
    "dailyQuota": 10,
    "monthlyQuota": 10,
    "tags": ["foo"],
    "metadata": {
      "fii": "bar"
    }
}
EOF

Then try to call the first service.

curl http://api.oto.tools:8080/api/\?foo\=bar \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJmb28iOiJiYXIifQ.lV130dFXR3bNtWBkwwf9dLmfsRVmnZhfYF9gvAaRzF8" \
-H "Otoroshi-Client-Id: api-apikey-id" \
-H "Otoroshi-Client-Secret: api-apikey-secret" \
-H "foo: bar" | jq

This will returns the list of the received headers by the mirror.

{
  ...
  "headers": {
    ...
    "my-expr-header.date": "2021-11-26T10:54:51.112+01:00",
    "my-expr-header.ctx.foo": "no-ctx-foo",
    "my-expr-header.env.path": "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
    "my-expr-header.apikey.id": "admin-api-apikey-id",
    "my-expr-header.apikey.tag": "one-tag",
    "my-expr-header.service-id": "expression-language-api-service",
    "my-expr-header.apikey.name": "Otoroshi Backoffice ApiKey",
    "my-expr-header.config.port": "8080",
    "my-expr-header.request.uri": "/api/?foo=bar",
    "my-expr-header.service-env": "prod",
    "my-expr-header.service-tld": "oto.tools",
    "my-expr-header.request.host": "api.oto.tools:8080",
    "my-expr-header.request.path": "/api/",
    "my-expr-header.service-name": "expression-language",
    "my-expr-header.ctx.foo-field": "no-ctx-foob-foo",
    "my-expr-header.ctx.useragent": "no-ctx-useragent.foo",
    "my-expr-header.request.query": "bar",
    "my-expr-header.service-group": "default",
    "my-expr-header.request.domain": "api.oto.tools",
    "my-expr-header.request.header": "bar",
    "my-expr-header.request.method": "GET",
    "my-expr-header.service-domain": "api.oto.tools",
    "my-expr-header.apikey.metadata": "bar",
    "my-expr-header.ctx.geolocation": "no-ctx-geolocation.foo",
    "my-expr-header.token.foo-field": "no-token-foob-foo",
    "my-expr-header.date-with-format": "2021-11-26",
    "my-expr-header.request.full-url": "http://api.oto.tools:8080/api/?foo=bar",
    "my-expr-header.request.protocol": "http",
    "my-expr-header.service-metadata": "no-meta-foo",
    "my-expr-header.ctx.default-value": "other",
    "my-expr-header.env.unknown-field": "not-found-java_h",
    "my-expr-header.service-subdomain": "api",
    "my-expr-header.token.unknown-foo": "no-token-foo",
    "my-expr-header.apikey.unknown-tag": "one-tag",
    "my-expr-header.ctx.unknown-fields": "not-found",
    "my-expr-header.token.unknown-fields": "not-found",
    "my-expr-header.request.unknown-query": "default value",
    "my-expr-header.service-unknown-group": "default",
    "my-expr-header.request.unknown-header": "default value",
    "my-expr-header.apikey.unknown-metadata": "default value",
    "my-expr-header.ctx.replace-field-value": "no-ctx-foo",
    "my-expr-header.token.unknown-foo-field": "not-found-foob",
    "my-expr-header.service-unknown-metadata": "default-value",
    "my-expr-header.config.unknown-port-field": "not-found",
    "my-expr-header.token.replace-header-value": "no-token-foo",
    "my-expr-header.ctx.replace-field-all-value": "no-ctx-foo",
    "my-expr-header.token.replace-header-all-value": "no-token-foo",
  }
}

Then try the second call to the webapp. Navigate on your browser to http://webapp.oto.tools:8080. Continue with user@foo.bar as user and password as credential.

This should output:

{
  ...
  "headers": {
    ...
    "my-expr-header.user": "User Otoroshi",
    "my-expr-header.user.email": "user@foo.bar",
    "my-expr-header.user.metadata": "roger",
    "my-expr-header.user.profile-field": "User Otoroshi",
    "my-expr-header.user.unknown-metadata": "not-found",
    "my-expr-header.user.unknown-profile-field": "not-found",
  }
}