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}
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.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
${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.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.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
${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
${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
${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.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",
}
}