Skip to main content

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_el(req.header.date).format('<format>')}get the date value from an el expression and formats it.
${date_el(req.header.date).format('<format>')}get the date value from an el expression and formats it.
Date information
${date}2021-11-25T10:53:25.366+01:00
${date.format('yyyy-MM-dd')}2021-11-25
${date_el(req.header.date).epoch_ms1747816667560

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.ip}the client ip address, the same as `${req.ip_safe}` unless `useLegacyClientIpAddress` is enabled, in which case it is the leftmost `X-Forwarded-For` entry when `trustXForwarded` is enabled and the connection address otherwise
${req.ip_address}same as `${req.ip}`
${req.ip_safe}the client ip address resolved according to the forwarded headers settings: the connection address when `trustXForwarded` is disabled, otherwise resolved through the trusted proxies when some are configured, or the first valid ip address of the client address header (`X-Forwarded-For` by default) when none is, falling back to the connection address when none is available
${req.ip_from_socket}the address of the peer that opened the connection, without any header involved
${req.ip_from_trusted_proxy}the client ip address resolved by walking the chain of the client address header (`X-Forwarded-For` by default, `Forwarded`, `X-Real-IP`, ...), only when `trustXForwarded` is enabled and the connection comes from a trusted proxy. falls back to the connection address
${req.ip_from_xff}the leftmost address of the `X-Forwarded-For` header, whatever sent it. falls back to the connection address
${req.ip_from_xforwarded_header}same as `${req.ip_from_xff}`
${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.ip}10.0.0.4
${req.ip_safe}10.0.0.4
${req.ip_from_socket}192.168.1.12
${req.ip_from_trusted_proxy}10.0.0.4
${req.ip_from_xff}10.0.0.4
${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.api.id}if the apikey consumes an api plan, the id of that api
${apikey.api.plan}if the apikey consumes an api plan, the id of that plan
${apikey.api.sub}if the apikey comes from a subscription, the id of that subscription
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, or 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.

Combining optional values​

It is possible to combine optional values (that are expression) in only one expression. Here the expression language will choose the first defined value, or fallback on the default value. The syntax it the following:

${<expr_1> || <expr_2> || <expr_3> || <expr_n> :: <default_value>}

of course you can ommit the :: <default_value> part if you don't want one.

${<expr_1> || <expr_2> || <expr_3> || <expr_n>}

If neither expression is defined, you will end with the bad-expr value.

For example if you're looking for a value that can be located either in headers, cookies and query params of the request you can write something like:

${req.headers.foo || req.query.foo || req.cookies.foo :: no-foo}

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/v18.0.1/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",
}
}