Security service and filter
The security-filter bundle has been created to protect specific API (graphql/rest/views, and others) from unauthorized usage, potential XSS/CSRF attacks and provide support for CORS requests.
It prevents API from being called from anywhere, first in a generic way through a global CORS filter, then per API configuration.
It also allows users to specify reduced scopes when using tokens. They can choose what API can be called with a token.
The bundle provides a service which can be used by different API providers to secure access.
A centralized configuration file provides rules defining how the APIs can be called.
Authorization configuration
The configuration is made up of a list of scopes. Each scope is granting some API access, based on API name, node path or types, or any other criteria that can be used by APIs.
If it does not hold any scope granting the API, access will be denied.
Scopes are granted and associated to a request with an explicit token, or automatic rules.
Personal API token or JWT token can hold scopes. For example you can grant a token to get server status, but not to undeploy a module.
Scopes can be automatically granted based on browser origin : some scopes are granted when called by same origin, or from a trusted origin.
They can be restricted to some specific user profiles : some scopes are available only to administrators, editors or privileged users
The configuration files are located in digital-factory-data/karaf/etc
, with the org.jahia.bundles.api.authorization-*.(yml|cfg)
filename pattern.
Starting from Jahia 8.1.0.0 you can write configuration in yaml format.
The following snippet :
myscope: description: Can access some graphql API metadata: visible: true auto_apply: - origin: hosted grants: - api: graphql.MyGqlType node: none
will be written this way when using cfg format :
myscope.description = Can access some graphql API myscope.metadata.visible = true myscope.auto_apply[0].origin = hosted myscope.grants[0].api = graphql.MyGqlType myscope.grants[0].node = none
Examples below are given in yaml format.
Scope name, description and metadata
Every scope must have a unique name. The description explains what the scope is granting.
Metadata can be freely added and used by UI or other services. For example, `visible` metadata makes the scope visible in Personal Api Tokens administration UI.
Scope grants
A scope contains a list of grants, one for each API access. A grant can contain one or more conditions - in order for the permission to be granted, all conditions must match.
You can use one these conditions, or both of them in the same grant :
-
api
: The names of the API (in a comma separated list), if the rule should only apply to some entry points. Different API names are provided by the API services. For example : ajax views (view.<view-type>
), the JCRest API module (jcrestapi
), or the GraphQL API (graphql.<gql-type>.<gql-field>
)grants: - api: graphql.JcrNode, graphql.JcrProperty
You can also define
include
andexclude
sub entries :grants: - api: include: graphql exclude: graphql.GqlAdmin, graphql.JcrNode
This grant will apply to all graphql API calls, except the ones on GqlAdmin and JcrNode fields.
Access to excluded API can be granted by other scopes or grant entries. -
node
: Matches the API calls related to a node. You can specifynode: none
to only match API calls that do not return a node.
To match some nodes, you can use the following sub entries :pathPattern
/excludedPathPattern
: Regular expressions that will be tested on the node path.workspace
:live
ordefault
, only request on the specified workspace will match.nodeType
/excludedNodeType
: Only request on nodes of these type will match.withPermission
: Only request on nodes, where the user has this permission, will match.
grants: - node: pathPattern: /,/sites(/.*)?,/modules(/.*)?,/mounts(/.*)? excludedPathPattern: /sites/[^/]+/users(/.*)?
You can combining multiple conditions in one grant :
yaml grants: - api: graphql node: none
This will allow all Graphql calls that do not involve a node.
Beware that this is completely different from creating multiple grants with one condition :
yaml grants: - api: graphql - node: none
This will allow all Graphql calls, and all calls that do not involve a node.
Auto-apply rules
Scope can be automatically applied to requests based on an origin. It's checked against the Origin
and Referer
headers.
hosted
or same
mean that the rule will match if the request is coming from the same server.
auto_apply: - origin: hosted - origin: http://www.mysite.com
It's also possible to always apply the scope, whatever the request is. It can be used to have API that will always be granted.
auto_apply: - always: true
User constraints
Some scopes are only usable by specific users. You can set which permission a user should have on a node :
constraints: - user_permission: manageModules path: /sites workspace: live
Or simply restrict the scope to privileged users :
constraints: - privileged_user: true
The scope will be available only to users who fulfill the constraints. It will never be applied for other users.
Configuration profiles
The user can choose a predefined security profile by setting a value in security.profile
, in org.jahia.bundles.api.security.cfg
file. These profiles can be found [here](`src/main/resources/META-INF/configuration-profiles`). These profiles can be found here.
- "default" profile is recommended one. It will not allow any API call from external origin, and from non-privileged users.
- "compat" profile is more open and is compatible with the previous security-filter implementation. Most graphql/rest calls are allowed for any user
- "open" profile allows every call.
It's also possible to not use any profile (everything will be denied by default) - you will have to fully provide your own configuration. Without any configuration Jahia GUI will not be work.
Legacy mode and migration report
The legacy mode can be used to keep the exact same behaviour as the previous version. It can be enabled by setting security.legacyMode=true
into org.jahia.bundles.api.security.cfg
.
The old org.jahia.modules.api.permissions-*.cfg
files will be used as before.
Note that you must have all permissions.cfg
files required to run your application, as the new authorization files won't be read anymore. Enabled the file org.jahia.modules.api.permissions-default.cfg.disabled to restore default legacy configurations.
Legacy configuration documentation can be found here.
Reporting in the logs can be enabled with security.migrationReporting=true
, to check what API call that was allowed with legacy mode, will be denied with the new configuration, or the opposite.
The configuration effectively being used is still the one defined by legacyMode
- the migrationReporting
option only add logs to tell what would have happened with legacyMode
set to the opposite value.
This is useful when migrating, if you were using API calls and you are unsure if they will still pass.
Reporting can be enabled when running in standard mode (security.legacyMode=false
) - it will continue to report when there's a difference between legacy and standard mode.
Debugging
In order to understand why a call is granted or not, you can set the org.jahia.bundles.securityfilter.core
package (or org.jahia.bundles.securityfilter.legacy
, if using legacy mode) to debug in log4j configuration.
This will enable log for every permission check, with the API that is being checked and the result, and the grant that matches, if any.
If no grant match, the list of enabled scopes will give you the information on which grants were unsuccesfully checked.
Configuration in a module
A module can package a configuration file in META-INF/configurations folder. Since DX version 7.2.2.0, all files in this folder are deployed in karaf/etc
at module startup.
Extending existing scope
It's possible to extends an existing scope in another configuration file, in order to add grants or auto-apply rules.
You just need to redeclare the scope, and the list of grants/rules you want to add :
graphql: auto_apply: - origin: http://www.mytrusted-origin.com
Personal API token
Personal API token provide authentication and can hold scopes. They can be used to make API calls. More documentation can be found at Using personal API tokens
JWT tokens (deprecated)
A request can be granted a scope through the usage of JWT tokens, passed in the Authentication: Bearer
header.
JWT Tokens contain a verified list of scopes, along with restriction on its usage. It's possible to restrict the usage of a token based on the client IP or referer header.
Configuration
When Jahia is started in development mode, tokens can be generated from the "Developer Tools" via the section "JWT Configuration" - the user can specify the list of scopes that will be owned by the token, and fill in the optional restrictions.
org.jahia.bundles.jwt.token.cfg
configuration file before generating any token.The file contains the following properties :
jwt.issuer
: Name of your organization, that will be included in tokens, only for informational purposejwt.audience
: The target audience is an identifier for you DX installation - audience is included in the token at generation, and only tokens with the same audience will be accepted.jwt.algorithm
: Algorithm used to sign the token. Only HMAC supported.jwt.secret
: Secret key used to be used with HMAC. It will be used to sign and validate tokens. You must change the secret and keep it safe - any token signed with the same secret can be accepted and will grant the associated scopes.
JWT example
A module can expose a scope, that will be granted with JWT token. In order for the scope to be applied, the client will have to provide a valid token containing the corresponding scope
claim.
The jwtConfiguration
tool will be used to generate the token. In this example, scope is getaway
, and we will add more restrictions on the referer field, so that the token can only be used when being used from a site on http://localhost
or http://127.0.0.1
.
Generated token will look like that : eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJodHRwOi8vamFoaWEuY29tIiwic3ViIjoiYXBpIHZlcmlmaWNhdGlvbiIsInJlZmVyZXIiOlsiaHR0cDovLzEyNy4wLjAuMSIsImh0dHA6Ly9sb2NhbGhvc3QiXSwiaXNzIjoiZHgiLCJzY29wZXMiOlsiZ2V0YXdheSJdLCJpYXQiOjE1Mzg0NjU3NjQsImp0aSI6ImJiNjUyYmI2LTVlOGUtNGRmZC1hYjI3LWRlYzY4NWQxZmVmYiJ9.YolJyuSXGlvIN9_hL4eH6D9_oFHKwt005y3vfCuR2ZU
The content of the token can be verified on jwt.io :
{ "aud": "http://jahia.com", "sub": "api verification", "referer": [ "http://127.0.0.1", "http://localhost" ], "iss": "dx", "scopes": [ "getaway" ], "iat": 1538465764, "jti": "bb652bb6-5e8e-4dfd-ab27-dec685d1fefb" }
The claims aud
and iss
are coming from the configuration file. You can also check the signature on jwt.io - here the token is signed with the default key my super secret secret
. It must match the secret in the configuration file.
iat
is the date of issue, and jti
is a unique token identifier. They could be used to set an expiration time or manually revoke a specific token, although the current implementation does not support it yet.
Finally, the application will add the token to its Authentication: Bearer
header, as in index.js .
Checking API authorization
Graphql
Graphql provider use the security-filter service to check every field access. The API name is built from the graphql type and the requested field : graphql.<gql-type>.<gql-field>
.
When a graphql field returns a JCR node or a list of JCR nodes, it filters the result based on API authorization on these nodes.
JCRest API
JCRest API filters all result based on security-filter configuration. API name is jcrestapi.<query-type>
.
Views
A render filter catches all ajax calls to *.json
and *.html.ajax
. The filter calls the service to check if the request is allowed or not.
The API name contains the template type and the name of the view itself : view.<template type>.<view name>
So for example, the following rule will apply to all requests on the tree.json view :
- api: view.json.tree
The following rule will match all json views on pages :
- api: view.json node: nodeType: jnt:page
Adding API checks to your API
The bundle exposes an OSGi service implementing org.jahia.services.securityfilter.PermissionService
.
In order to check an API call, you should call the hasPermission
method, with a query
map parameter.
The query map contains information that describes your API call, and will be tested against the different grants
:
- It must at least contain the
api
entry, with a string describing it in a dot-separated fashion :my-api.type.sub-type
. It is tested by theApiGrant
class. - It can optionally contains a
node
entry, with aJCRNodeWrapper
value. This one is tested by theNodeGrant
class.
Other Grant
implementations may check other entries.
CORS Filter
Security-filter module embeds a global CORS filter. It is based on tomcat implementation, and can use all configuration settings described here : CORS Filter.
These settings must be set in the org.jahia.bundles.api.security.cfg
file.
CSRF Tokens
Jahia protects all action triggering URLs having the .do
suffix against CSRF attacks with the usage of secure random tokens. This is handled by the Jahia CSRF Guard module, which is installed on default.
How it works
With the help of webfilters we modify the HTML of pages including action links or form submissions, to obtain a dynamic javascript. You will see the following line in the HTML of such a page:
<script type="text/javascript" src="/modules/CsrfServlet"></script>
This javascript detects forms and links and will add the CSRF token. It's added by a filter and should be in all pages served by Jahia.
- In forms, a hidden field is added :
<form method="POST" action="/sites/digitall/home/csrftest/area-main/dummy-csrf-action-caller.modifyTextAction.do">
<input type="hidden" name="jcrNodeType" value="jnt:testContent">
<input type="hidden" name="jcrRedirectTo" value="/sites/digitall/home/csrftest">
<input type="hidden" name="jcrResourceID" value="aacd982b-0bc4-46e6-8f25-3c6fa732a31f">
<input type="hidden" name="newText" value="Hello Planet!">
<button type="submit">Say Hello Planet</button>
<input type="hidden" name="CSRFTOKEN" value="D3C4-2YHX-RR1R-2C64-3T74-T04V-5VL0-0T0G">
</form>
- In tags containing src or href attribute, the token is added in the URL :
- For XHR calls, the token is transparently added into an HTTP header when the call is executed :
Disabling CSRF-guard for an action
There may be cases, where a CSRF token is not needed for an action, for instance when the action does not trigger any state changes.
For that a configuration file can be packaged directly in your module. This is the best way to allow CsrfGuard executing your actions as these settings will be used on module deployment. Here is a quick way to do it:
- First create a new configuration folder
src/main/resources/META-INF/configurations
- Create a new file
org.jahia.modules.jahiacsrfguard-yourModule.cfg
in this folder. Note this filename needs to be unique as it will be deployed in yourdigital-factory-data/karaf/etc
. So we suggest replacing yourModule with the name of your module. So for instance if your module name is test-module then you should create such a filesrc/main/resources/META-INF/configurations/org.jahia.modules.jahiacsrfguard-test-module.cfg
- Edit this new configuration file, and whitelist all your action URLs, which will not require a CSRF token, with such a line:
whitelist = *.action1.do,*.action2.do
Allow GET methods
By default, all the actions are restricted just to POST requests. You can explicitly declare that GET is allowed in your spring file with requiredMethods="GET,POST"
Override CSRF guard configurations
Since versions 1.5.0 and 2.4.0 of the jahia-csrf-guard module, you can override the csrfguard properties in /karaf/etc/org.owasp.csrfguard.cfg
.
For instance with
org.owasp.csrfguard.Enabled = false
you can (temporarily) disable the CSRF filter without having to stop the jahia-csrf-guard module.
Error when referer check does not match the protocol
In the case where your site is accessed via SSL protocol (https) only up to a front-end proxy and then towards the Jahia just with http, you may see CSRF related errors looking like this in the log.
ERROR [CsrfGuard] - Referer domain https://<your-page> does not match request domain: http://<your-page>
This should be solved by following the recommendation in the knowledge base article: Jahia links are not in https
Alternatively you could also set the following property in /karaf/etc/org.owasp.csrfguard.cfg
:
org.owasp.csrfguard.JavascriptServlet.refererMatchProtocol = false
Using tokens per-page
From jahia-csrf-guard version 3.0.0 onwards it is possible to enable the creation of random unique tokens per-page (and session) as opposed to just a unique per-session prevention token, which then is the same on all pages. This is a defense in depth strategy to limit the impact, whenever a token gets leaked to an attacker. With a leaked token per-session a CSRF attack could be carried out against any form or action in the entire webapp, as long as the victim's session is active. With a token per-page the CSRF attack could only be carried out against a small subset of resources.
Up to and including all Jahia 8.1.x versions, we keep the legacy token per-session implementation activated on default, because using tokens per-page requires testing and possible code changes within custom templates or modules. You should especially check that the back button of the browser or a form re-submit in the same session still works as expected. Furthermore there may be an issue if you call AJAX requests to actions during the page initialization phase (see this linked discussion to get hints about possible solutions). Also starting to use tokens per-page may reveal a mistake, when HTML fragments using tokens are getting cached in Jahia. You have to set a view related cache.expiration
property to 0
(see View Caching).
The usage of tokens per-page can get activated in Jahia 8.1.x with setting the following property in /karaf/etc/org.owasp.csrfguard.cfg
:
org.owasp.csrfguard.TokenPerPage = true