Authorisation for the Query Engine is based around JWTs.
There are three basic ways in which authorisation is intended to be used:
-
The Query Engine is embedded in the host application in the context of a user session and a prepared JWT is passed through by the host application.
-
The Query Engine is called directly by another system that can only provide Basic Authentication.
-
The Query Engine can be called without requiring any authorisation (some feed can be public).
Providing a JWT
There are five ways in which authorisation data can get to the Query Engine:
-
A bearer token can be provided via the standard Authentication header. This is the recommended approach when the query engine is accessed in the context of a user session in the host application. This handling can be disabled by setting the parameter enableBasicAuth to false;
-
Basic authentication can be used when the query engine is called directly. In this situation the passed in credentials will be used in making a client credentials grant to the host system. This handling can be disabled by setting the parameter enableBasicAuth to disabled;
-
If there is a proxy in front of the query engine that has already validated the JWT it can pass in the complete JWT payload as a base64 encoded header.
-
For the UI only, a session cookie may be generated that stores a JWT in the query engine database (and in a local cache), see OpenID Connect in the UI. The session cookie value itself is just 100 bytes generated by SecureRandom.
-
If there is no Authentication header and no OpenID Introspection header the query will be run without any authorisation data.
The security of all of these approaches is dependent on the headers reaching the query engine. It is very important that however the query engine is configured the ingress approach prevents the forging of relevant headers (i.e. X-Forwarded-Host).
Path Hijacking
The recommended approach for the ingress to the query engine is for all requests to a given path on the host to be sent directly to query engine, bypassing the main host service completely. This is easy to accomplish with most ingress proxies.
Basic Authentication
Basic authentication is not ideal because credentials are typically stored in plain (or recoverable) text by clients and those credentials have no time limit (unlike tokens). Basic authentication is also sent unencrypted and must never be used without a secure TLS configuration.configuration.
By default basic authentication is disabled, to enable it the parameter basicAuth.grantType must be set to either "clientCredentials" or "resourceOwnerPasswordCredentials": * clientCredentials The username/password in the request will be used to make a client credentials grant request to an IdP. * resourceOwnerPasswordCredentials The username/password in the request will be used to make a resource owner password credentials grant request to an IdP. Many IdPs disable or at least deprecate use of this grant type and it should be avoided if possible.
IdP Determination
When basic authentication is enabled the query engine must know the URL to use for the OAuth request. There are three ways in which this can be set.
-
basicAuth.idpMap This is a map of "domains" to IdP URLs that will be consulted if the username in the request is of the form "user@domain". The domain specified in the username is used as the key to this map, but serves no other purpose and does not have to be recognised by the IdP itself (the domain will be removed from the username when the request to the IdP is made).
-
basicAuth.defaultIdp A single IdP URL that will be used if the username in the basic auth header does not contain an '@'. This is expected to be set on any standalone deployment of the query engine.
-
Self discovery If neither of the alternative values is configured the query engine will make an OpenID Connect Discovery request to itself - starting at host on which the request was made (considering the X-Forwarded-Proto and X-Forwarded-Host headers if they exist, and the values supplied as jwt.issuerHostPath will be added to the host, enabling issuers that are not at the root path of the host). The authorization_endpoint found in the metadata will be used as the IdP URL. This is expected to be the usage for any deployment of the query engine that uses path hijacking.
Validation of JWTs
Every token will be validated before it is accepted by the query engine. This follows a fairly standard process, but there is some extra configuration of the query engine that governs it:
-
The JWT is parsed - split into three and each component base64 decoded.
-
The algorithm specified in the JWT header must match one of the permitted algorithms (RS256, RS384 and RS512 - this list could be extended if useful).
-
The public key must be found. There are two ways in which the query engine can find the public key:
-
If the parameter jwt.issuerHostPath is set or the parameter jwt.jwksEndpoints is empty the JWKS url will be sought by OpenID Connect Discovery using the Host at which the request was made (considering the X-Forwarded-Host header if it exists, and the values supplied as jwt.issuerHostPath will be added to the host, enabling issuers that are not at the root path of the host).
-
Otherwise all the endpoints listed in jwt.jwksEndpoints will be queried for JWKs. However the public key is found it will be cached, obeying the cache-control(max-age) HTTP headers and used from the cache in future. If a JWT is received that references a JWK that is not found in the cache the discovery process will be run again.
-
-
The issuer must either match jwt.acceptableIssuerRegexes or be found in the file specified by jwt.acceptableIssuersFile. The regex approach is aimed at design mode and test environments, production environments should prefer jwt.acceptableIssuersFile (the jwt.filePollPeriodDuration can be used to control how frequently the query engine rechecks the file).
-
The nbf and exp claims in the JWT must indicate that the token is currently valid.
-
The audience must match any one of the values specified by jwt.requiredAudiences (which defaults to just "query-engine").
-
The JWT must specify a subject (it must have a non-blank "sub" claim).
OpenID Connect in the UI
The UI for the Query Engine can be configured to use OpenID Connect, but this does not supersede the requirement for validation configuration.
OpenID Connect is configured using the session configuration option, the key part of which is the oauth map.
Any number of authentication endpoints may be configured. When a user attempts to login (which will happen on first access to the UI if requireSession is set) they will be presented with a list of all the auth endpoints to choose from.
After authenticating with their chosen provider the generated JWT will be stored in the Query Engine database and a random cookie (configured at sessionCookie) will be generated to refer to it.
Custom Certificates
It is quite likely that a secure configuration of the Query Engine will be required to trust custom root certificates (e.g. for JWT endpoints, especially in test environments).
It is recommended by CIS that all containers by run with a read only root filesystem, so updating the CA store on startup is awkward.
My preferred approach is to us this script:
image=query-engine-design-mode:0.0.48
javahome=/qe-java
docker run -it --entrypoint /bin/ash -v $(pwd):/out query-engine-design-mode:0.0.48-main -c "cp /qe-java/lib/security/cacerts /out"
docker run -it --entrypoint /bin/ash -v $(pwd):/out query-engine-design-mode:0.0.48-main -c 'for cert in /out/*.crt; do alias=${cert##*/} ; /qe-java/bin/keytool -keystore /out/cacerts -import -trustcacerts -storepass changeit -noprompt -alias ${alias%.*} -file ${cert} ; done'
Simply place all required certificates into the current directory, edit that script to have the correct image name, and then run the script. The result will be the cacerts file in the current directory containing all the CA certificates known to the JDK that Query Engines uses, with all your custom certs added to it.
Copy the resulting cacerts file somewhere safe and mount it over the top of the original cacerts file in your Query Engine compose file. My preferred way to do this is with a Docker Config:
configs:
- source: cacerts
target: /qe-java/lib/security/cacerts
mode: 0400