Introduction
I needed a TIBCO JasperReports® Server running in a Linux VM integrated in the MS AD-environment, including a Kerberos-based single-sign-on and with name and email attached to the external users in JasperServer.
With JasperReports Server 7.1.0 and it's builtin LDAP-support, some Wiki-pages, I thought this would be an easy task.
Ok, it wasn't for me, but I hope, it will be for you, now!
- As of 2018-08-23, this description is complete.
- As of 2022-10-27 this description is tested with JasperReports Server 8.1.0
- Search for 'Note 2022-10-27' and especially 'Note 2022-10-27 Changes from 7.1.0 to 8.1.0' to find the changes
- 2022-11-11 Added a note about increasing the session-timeout when using Kerberos-based login
Prerequisites
JasperReports® Server should run in it's own Linux VM
- You will need the password of a domain administrator to enter the domain with this Linux VM!
Users are maintained in an ActiveDirectory-domain with Kerberos enabled
Domain-users at client PCs are Kerberos-enabled
- This should be default for current Windows server and client systems
- I haven't tried any Linux Samba server or client settings
You know how to install JasperReports Server, set up a Linux VM, Apache, etc.
- You can use the system or bundled components of Tomcat, PostgreSQL, etc.
- I decided to use the system Tomcat and PostgreSQL services and bundled PhantomJS
Browsers should be Kerberos-enabled for the domain to handle the ticket
- E.g. for Firefox: Search for "Enabling Kerberos Authentication in Firefox" and you'll get the information how to set network.negotiate-auth.trusted-uris to your ".domain.com"
- Firefox, Chrome, Edge, even IE should work
I used (K)Ubuntu for the Linux-VM, so you might have to adopt some commands, eg. if you use another package management system.
If you like to have the emailAddress and fullname of your LDAP-users mapped to the JasperReports Server external users, you have to compile and pack a Java class in a jar and deploy it to your JasperReports Server
This has nothing to do with the waffle-based solution with a JasperReports Server running on a Windows-VM/server!
Overview
After struggling with my old (2011-2013) coded Kerberos/LDAP-implementation up to JasperReports Server 5.0, some Wiki-pages (thank you, if you wrote one!), Java policy-file etc. I decided to split Kerberos authentication, LDAP authorisation and JasperReports Server integration. This definitely eases the management and debugging of the authentication and authorization.
I came up with the following solution:
Level | User/Status |
---|---|
Apache (2.4) | (automatic) Kerberos-based authentication resulting in user user |
| Fallback to BASIC auth, if not in the domain |
| The next levels can access the authenticated user by the header field X-Remote-User |
| |
| Restrict the general access to the web-server |
Tomcat 8(either system or JasperReports Server included) | Named authenticated user |
| No authentication by Tomcat! |
| No side-injection of not-authenticated users |
JasperReports Server 8 | Named authenticated JS user with AD groups |
| username = X-Remote-User |
| roles = ROLE_LDAP_groups |
Custom UserSetupProcessor | Named authenticated JS user with full name and email-address |
externalInetOrgPersonUserSetupProcessor using InetOrgPerson properties | |
Uses LDAP to resolve name and mail-address | fullname = LDAP displayName |
Advantages
- (Automatic) Single-Sign-On for all domain users, fallback to BASIC authentication for none-domain access
- Even JasperReports Server's great admin-feature "log in as user" works!
- User and groups can be set up and maintained in AD
- Permissions of reports can be assigned to certain AD users or (better!) to AD groups via their mapped roles
- Mails can be sent to all users with a correct mail-address in their AD account
- Manual login in JasperServer via the /jasperserver/login.html page with a local admin is still possible and you can use the "Login as user" for LDAP users.
To login with Kerberos again, just logout and go back to /jasperserver, and you're in as Kerberos-user.
Setup Kerberos-enabled Linux
IP and hostname
Get IP and FQ-hostname of your new jasperserver from your system administrator
- Modify /etc/hosts with your values
172.30.1.xxx jasperserver.domain.name jasperserver
Install necessary packages
sudo apt install krb5-user samba sssd ntp
Configure Kerberos and Samba
See https://help.ubuntu.com/lts/serverguide/sssd-ad.html for details.
Realm for Kerberos Version 5: | DOMAIN.COM |
---|---|
/etc/krb5.conf | Add: [realms] DOMAIN.COM = { kdc = domctrl.domain.com master_kdc = domctrl.domain.com admin_server = domctrl.domain.com default_domain = DOMAIN } [domain_realm] .domain.com = DOMAIN.COM domain.com = DOMAIN.COM |
/etc/ntp.conf | # Specify one or more NTP servers. server domctrl.domain.com |
/etc/samba/smb.conf Note 2022-10-27: | ### Active Directory workgroup = DOMAIN # netbios name = jasperserver # client signing = yes # client use spnego = yes kerberos method = secrets and keytab realm = DOMAIN.COM security = ads encrypt passwords = yes |
/etc/sssd/sssd.conf Note 2022-10-27: | [sssd] services = nss, pam config_file_version = 2 domains = DOMAIN.COM [domain/DOMAIN.COM] id_provider = ad access_provider = ad # Use this if users are being logged in at /. # This example specifies /home/DOMAIN-FQDN/user as $HOME. Use with pam_mkhomedir.so override_homedir = /home/%d/%u # Uncomment if the client machine hostname doesn't match the computer object on the DC. # ad_hostname = jasperserver.domain.com |
Permissions /etc/sssd/sssd.conf | sudo chown root:root /etc/sssd/sssd.conf sudo chmod 600 /etc/sssd/sssd.conf |
Optional: | Append after session required pam_unix.so session required pam_mkhomedir.so skel=/etc/skel/ umask=0022 |
/etc/hosts (see above) | 172.30.1.xxx jasperserver.domain.com jasperserver If you don’t regard the sequence, with net ads join you might get |
Restart Services
sudo systemctl restart ntp.service sudo systemctl restart smbd.service nmbd.service sudo systemctl start sssd.service
Troubleshooting Samba
Just in case…
https://wiki.samba.org/index.php/Troubleshooting_Samba_Domain_Members
Let the Linux-VM join the AD domain
-------------------------------------------------------- | |
Restart Services | sudo systemctl restart ntp.service sudo systemctl restart smbd.service nmbd.service sudo systemctl start sssd.service |
Log in as Domain-Admin | sudo kinit Administrator |
Join the AD domain Note 2022-10-27: | sudo net ads join –k sudo net ads keytab add HTTP -k |
Check SPNs Note 2022-10-27: | sudo klist -e -k -t /etc/krb5.keytab |
Change permissions for Keytab file | sudo chmod 640 /etc/krb5.keytab sudo chgrp www-data /etc/krb5.keytab |
Apache2-Restart (if it’s already configured) | sudo systemctl restart apache2 |
Log in to the Linux-machine as domain user
Right now, you should be able to log into the Linux-VM as any domain user. This is a good test, if Kerberos is working, but it might not be the desired behavior in your case.
Check out your Linux documentation on how to change it the way you need it.
Setup Kerberos enabled Apache
Install necessary packages
sudo apt install apache2 libapache2-mod-auth-kerb
Activate AuthLDAPURL and RequestHeader
RequestHeader is needed e.g. for JasperServer
sudo a2enmod authnz_ldap headers
Activate Proxy modules
You'll need it later to connect Apache
sudo a2enmod proxy proxy_ajp
Secure a Location with Kerberos plus BASIC-fallback
/etc/apache2/sites-available/000-default.conf (or the one you use)
Change the lines starting "->" to match your values.
<Location / > AuthType Kerberos AuthName "Mit Windows-Konto einloggen! Login with Windows-Account!" # Browser Negotiate allowed KrbMethodNegotiate on # Fallback allowed (for Mobile- or other access (from another server, etc)) KrbMethodK5Passwd on KrbSaveCredentials on -> KrbAuthRealms DOMAIN.COM KrbServiceName HTTP Krb5Keytab /etc/krb5.keytab # Make sure that Kerberos passes the username only into Apache rather than the username@REALM # This is important if you’re doing additional authorization via LDAP as discussed below. KrbLocalUserMapping on # LDAP URL and Request-User-Credentials -> AuthLDAPURL "ldap://domctrl.domain.com/dc=domain,dc=com?sAMAccountName" # LDAP-Query-User (Note: "AuthLDAPInitialBindAsUser on" can not be used as an alternative!) -> AuthLDAPBindDN KerberosUser@domain.com -> AuthLDAPBindPassword thePasswordOfTheKerberosUser # Search parameters AuthLDAPGroupAttribute member AuthLDAPGroupAttributeISDN On # AuthLDAPMaxSubGroupDepth 5 AuthLDAPSubGroupClass group -> # any authenticated user allowed (comment this out, if you use require ldap-group -> require valid-user -> # You might (alternatively to require valid-user) want to restrict the access to certain LDAP groups -> ## require ldap-group CN=GroupWithJasperAdmins,OU=subdepartment,OU=department,DC=domain,DC=com -> ## require ldap-group CN=GroupWithJasperUsers,OU=subdepartment,OU=department,DC=domain,DC=com # Forward the username for "Auto-LDAPing"=Auto-Login with JasperServer RequestHeader set X-Remote-User expr=%{REMOTE_USER} </Location>
RequestHeader X-Remote-User (for JasperServer)
The RequestHeader directive
RequestHeader set X-Remote-User expr=%{REMOTE_USER}is needed for JasperServer only! Tomcat itself uses the credentials if tomcatAuthentication="false" is set as described below
Restart Apache
sudo systemctl restart apache2
Needed every now and then...
And open your servers URL http://jasperserver.domain.com to check, if the Apache is working fine.
System Tomcat or JasperServer Tomcat?
You can alternatively use the system Tomcat or the Tomcat included in the JasperReports Server installation. The latter is my preference for development and debugging, because you don't have to struggle with Java-versions or other problems unrelated to your primary task.
Official guide says: The Binary Installer is intended for evaluation purposes only. To install TIBCO JasperReports Server for enterprise production environments, use the stand-alone WAR file distribution, which is the official TIBCO JasperReports Server installer. This implies that using your own (system) Tomcat is strongly recommended for Production environments.
You should install JasperServer according to your choice using the internal or system components.
If you like to install both editions, don't forget to switch off the one you do not need and enable the other one.
Installation paths for external components
PhantomJS | /usr/bin/phantomjs |
---|---|
PostgreSQL | /usr/lib/postgresql/10/bin |
Tomcat 8 | /var/lib/tomcat8 |
Note 2022-10-27:
I haven't checked these with Ubuntu 22.04
Switch system Tomcat on or off
(Re-)start system Tomcat
sudo systemctl restart tomcat8.service
Stop system Tomcat
sudo systemctl stop tomcat8.service
Switch JasperServer-included Tomcat on or off
(Re-)start JasperServer-included Tomcat:
./ctlscript.sh restart tomcat
Stop JasperReports Server included Tomcat:
./ctlscript.sh stop tomcat
Install system Tomcat
Install necessary packages
sudo apt install tomcat8
Optional stuff:
sudo apt install tomcat8-admin tomcat8-docs tomcat8-examples
- Includes:
- Admin-GUI tomcat8-admin
- Documentation tomcat8-docs
- Examples tomcat8-examples
Ease your life by entering the group tomcat8
sudo usermod -aG tomcat8 <myusername>
Do this for PostgreSQL, too, if you like
sudo usermod -aG postgres <myusername>
When using internal Tomcat installed with sudo to /opt
Make the Tomcat 8 conf-directory accessible for users of the Tomcat8 group:
sudo chmod g+x /opt/jasperreports-server-cp-7.1.0/apache-tomcat/conf
And re-login to your Linux!
Setup Tomcat using Apache authenticated username
Tomcat's own authentication must be disabled:
tomcatAuthentication="false"
External access from other hosts than localhost must be prevented:
address="127.0.0.1"
Change conf/server.xml in <Service name="Catalina">
<Connector port="8080" URIEncoding="UTF-8" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" address="127.0.0.1" /> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" tomcatAuthentication="false" address="127.0.0.1"/>
If you use JasperServer Studio, please see the Known Issues.
Debug: Change the Tomcat index.jsp-file to show the username
The JasperReports Server includes a Tomcat ROOT webapp which provides a nice index.jsp page. The Ubuntu packages only installs a simple index.html.
Use index.jsp-ROOT instead of index.html-ROOT
Rename /var/lib/tomcat8/webapps/ROOT
sudo mv /var/lib/tomcat8/webapps/ROOT /var/lib/tomcat8/webapps/ROOT_from_package
Copy JasperReports Servers webapps/ROOT to /var/lib/tomcat8/webapps/ROOT and fix permissions
sudo cp -r <jasperreports-server>/apache-tomcat/webapps/ROOT /var/lib/tomcat8/webapps/ROOT sudo chown -R root:tomcat8 /var/lib/tomcat8/webapps/ROOT
Show the username of the authenticated user
In the webapps/ROOT/index.jsp find the line (it was line 52 for me)
<h2>If you're seeing this, you've successfully installed Tomcat. Congratulations!</h2>
and replace it with (something like)
<h2>Hello user '${pageContext.request.remoteUser}', if you're seeing this, you've successfully installed Tomcat. Congratulations!</h2> <p>User remote: <%=request.getRemoteUser()%></p> <p>User principal: <%=request.getUserPrincipal()%></p>
Connect Apache with Tomcat via ProxyPass and forward username to JasperReports Server
For the Tomcat connection you have to change the location (if you need), the ProxyPass-settings and the RequestHeader directive.
In your productive system, you only might want to forward the location /jasperserver, the ProxyPass is as follows
# Important: Deactivate Tomcat´s own authentication by AJP and restrict access to localhost in server.xml: tomcatAuthentication="false" address="127.0.0.1" # <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" tomcatAuthentication="false" address="127.0.0.1" /> ProxyPass ajp://localhost:8009/jasperserver ProxyPassReverse ajp://localhost:8009/jasperserver
# Forward username as RequestHeader RequestHeader set X-Remote-User expr=%{REMOTE_USER}
Final Tomcat-connection configuration in Apache
# Integration Tomcat <IfModule mod_proxy_ajp.c> <Location /jasperserver> # Important: Deactivate Tomcat´s own authentication by AJP and restrict access to localhost in server.xml: tomcatAuthentication="false" address="127.0.0.1" # <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" tomcatAuthentication="false" address="127.0.0.1" /> ProxyPass ajp://localhost:8009/jasperserver ProxyPassReverse ajp://localhost:8009/jasperserver AuthType Kerberos AuthName "JasperServer: Mit Windows-Konto einloggen! Login with Windows-Account!" # Browser Negotiate allowed KrbMethodNegotiate on # Fallback allowed (for Mobile- or other access (from another server, etc)) KrbMethodK5Passwd on KrbSaveCredentials on -> KrbAuthRealms DOMAIN.COM KrbServiceName HTTP Krb5Keytab /etc/krb5.keytab # Make sure that Kerberos passes the username only into Apache rather than the username@REALM # This is important if you’re doing additional authorization via LDAP as discussed below. KrbLocalUserMapping on # LDAP URL and Request-User-Credentials -> AuthLDAPURL "ldap://domctrl.domain.com/dc=domain,dc=com?sAMAccountName" # LDAP-Query-User (Note: "AuthLDAPInitialBindAsUser on" can not be used as an alternative!) -> AuthLDAPBindDN KerberosUser@domain.com -> AuthLDAPBindPassword thePasswordOfTheKerberosUser # Search parameters AuthLDAPGroupAttribute member AuthLDAPGroupAttributeISDN On # AuthLDAPMaxSubGroupDepth 5 AuthLDAPSubGroupClass group -> # any authenticated user allowed (comment this out, if you use require ldap-group -> require valid-user -> # You might (alternatively to require valid-user) want to restrict the access to certain LDAP groups -> ## require ldap-group CN=GroupWithJasperAdmins,OU=subdepartment,OU=department,DC=domain,DC=com -> ## require ldap-group CN=GroupWithJasperUsers,OU=subdepartment,OU=department,DC=domain,DC=com # Forward the username for "Auto-LDAPing"=Auto-Login with JasperServer RequestHeader set X-Remote-User expr=%{REMOTE_USER} </Location> </IfModule>
Test it with a redirect
For testing purposes, you probably prefer a redirect of the root location / as follows
<IfModule mod_proxy_ajp.c> <Location / > ProxyPass ajp://localhost:8009/ ProxyPassReverse ajp://localhost:8009/ [...]
Open http://jasperserver.domain.com to access your Tomcat index.jsp
This worked once, but not anymore (don't know why):
If you open http://jasperserver.domain.com (with or without trailing slash), you'll see the Apache frontpage. Just append a question mark to get the Tomcat index.jsp page: http://jasperserver.domain.com/? (again: The slash doesn't matter: With http://jasperserver.domain.com? you will be redirected)
Are you authenticated?
If everything with configured correctly (and you have restarted Apache and Tomcat), at http://jasperserver.domain.com/? you should see something like:
Hello user 'username', if you're seeing this, you've successfully installed Tomcat. Congratulations!
User remote: username
User principal: CoyotePrincipal[username]
Great! How about a snapshot of the VM right now?
- To be sure: Just comment out require valid-user in your /etc/apache2/sites-available/000-default.conf, restart your Apache sudo systemctl restart apache2 and try again: Ups, internal server error. Change it back, restart Apache and it's working again!
- If it's not working: Did you allow your browser to negotiate the tickets?
Install JasperReports Server with LDAP-support
You might have an installed installation of JasperReports Server. Then just adopt it to the Tomcat installed above.
Enable LDAP
In either case, you must add the LDAP settings to your buildomatic/default_master.properties below part 9. The values are the same you already used in the Apache-configuration, without the parameter "?sAMAccountName" in the URL.
# 9) External Authentication Data Sources # ... external.ldapUrl=ldap://domctrl.domain.com/dc=domain,dc=com external.ldapDn=KerberosUser@domain.com external.ldapPassword=thePasswordOfTheKerberosUser
Then (re-)install this with
cd buildomatic sudo ./js-install-ce.sh minimal
or
cd buildomatic sudo ./js-install-ce.sh regen-config
Take care: Calling js-install-ce.sh without a parameter will install the demo databases!
Check, if you can access the JasperServer, get to the login-page and be able to log in. (See the JasperReports Server installation guide for details)
Change webapp configuration if not applied
Sometimes, these settings are not applied by using buildomatic.
You might append or change WEB-INF/js.config.properties with the same lines used with '9) External Authentication Data Sources'.
Enable Pre-Authentication with LDAP-username
Now you need the following /var/lib/tomcat8/webapps/jasperserver/WEB-INF/applicationContext-externalAuth-LDAP.xml
Save the file and fix the permissions with
sudo chown -R root:tomcat8 /var/lib/tomcat8/webapps/jasperserver/WEB-INF/applicationContext-externalAuth-LDAP.xml
/jasperserver/WEB-INF/applicationContext-externalAuth-LDAP.xml
I tried to write some useful comments in this file, so it's hopefully self-explanatory.
Note 2022-10-27 Changes from 7.1.0 to 8.1.0
7.1.0 | 8.1.0 |
---|---|
<ref local= | <ref bean= Replace all occurrences |
<beans ... xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> | <beans ... http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"> |
<bean id="proxyBasicProcessingFilter" class="com.jaspersoft.jasperserver.api.security.externalAuth.ExternalAuthBasicProcessingFilter" parent="basicProcessingFilter"> <property name="authenticationManager" ref="ldapAuthenticationManager"/> | <bean id="proxyBasicProcessingFilter" class="com.jaspersoft.jasperserver.api.security.externalAuth.ExternalAuthBasicProcessingFilter" parent="basicProcessingFilter"> <constructor-arg index="0"><ref bean="ldapAuthenticationManager"/></constructor-arg> <!-- not working from sample file: <property name="authenticationManager" ref="ldapAuthenticationManager"/> --> |
<bean id="ldapAuthenticationManager" class="com.jaspersoft.jasperserver.api.security.externalAuth.wrappers.spring.JSProviderManager"> <property name="providers"> <list> <ref local="preAuthProvider"/> <ref bean="${bean.daoAuthenticationProvider}"/> </list> </property> | <bean id="ldapAuthenticationManager" class="com.jaspersoft.jasperserver.api.security.externalAuth.wrappers.spring.JSProviderManager"> <constructor-arg index="0"> <list> <ref bean="preAuthProvider"/> <ref bean="${bean.daoAuthenticationProvider}"/> </list> </constructor-arg> |
<bean id="externalUserSetupProcessor" ...> ... </bean> | Remove the bean Or get it from the sample file and fix it, if you'd like to try the test below |
<!-- ~ Copyright (C) 2005 - 2022 TIBCO Software Inc. All rights reserved. ~ http://www.jaspersoft.com. ~ ~ Unless you have purchased a commercial license agreement from Jaspersoft, ~ the following license terms apply: ~ ~ This program is free software: you can redistribute it and/or modify ~ it under the terms of the GNU Affero General Public License as ~ published by the Free Software Foundation, either version 3 of the ~ License, or (at your option) any later version. ~ ~ This program is distributed in the hope that it will be useful, ~ but WITHOUT ANY WARRANTY; without even the implied warranty of ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ~ GNU Affero General Public License for more details. ~ ~ You should have received a copy of the GNU Affero General Public License ~ along with this program. If not, see <http://www.gnu.org/licenses/>. --> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd"> <!-- ############ PreAuthentication with LDAP-based userdetails ############ - Configuration of pre-authentication via an external LDAP server. --> <!-- ############ PreAuth ############ --> <!--https://community.jaspersoft.com/wiki/configuring-ldap-using-http-headers-jasperreports-server-60-sso-framework--> <!-- This pre-authentication header will handle LDAP authentication using a request header --> <bean id="preAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider"> <property name="preAuthenticatedUserDetailsService"> <ref bean="wrappedUserDetailsService"/> </property> <property name="throwExceptionWhenTokenRejected" value="false"/> </bean> <!-- This wrapped user details service is used by the preauth provider defined above and provides a hook into the LdapUserDetails Service --> <bean id="wrappedUserDetailsService" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> <property name="userDetailsService"> <ref bean="ldapUserDetailsService"/> </property> </bean> <!-- This LdapUserDetailsService creates UserDetails objects using the userSearch and ldapAuthoritiesPopulator beans and creates a User-object according to the userDetailsMapper --> <bean id="ldapUserDetailsService" class="org.springframework.security.ldap.userdetails.LdapUserDetailsService"> <constructor-arg index="0"> <ref bean="userSearch" /> </constructor-arg> <constructor-arg index="1"> <ref bean="ldapAuthPopulator" /> </constructor-arg> <property name="userDetailsMapper" ref="userDetailsMapper"/> </bean> <!-- InetOrgPersonContextMapper is needed for the emailAddress --> <bean id="userDetailsMapper" class="org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper" /> <!-- <bean id="userDetailsMapper" class="org.springframework.security.ldap.userdetails.LdapUserDetailsMapper" /> --> <!-- This filter will be added to the wildcard filter chain to intercept the request headers and pass them to the authentication manager --> <bean id="proxyPreAuthenticatedProcessingFilter" class="com.jaspersoft.jasperserver.api.security.externalAuth.preauth.BasePreAuthenticatedProcessingFilter"> <!-- The name of the principalRequestHeader field must fit the name set in the Apache configuration: RequestHeader set X-Remote-User expr=%{REMOTE_USER} --> <property name="principalParameter" value="X-Remote-User"/> <property name="tokenDecryptor" ref="plainTextTokenDecryptor"/> <!-- Configuration determining whether principalParameter is read from header (default) or from request parameter. --> <property name="tokenInRequestParam" value="false"/> <property name="authenticationManager" ref="ldapAuthenticationManager"/> <property name="externalDataSynchronizer" ref="externalDataSynchronizer"/> </bean> <!-- Username is unencrypted --> <bean id="plainTextTokenDecryptor" class="com.jaspersoft.jasperserver.api.common.crypto.DevelopmentPlainTextNonCipher"/> <!-- Must be disabled. Refer to https://community.jaspersoft.com/wiki/jasperreports-server-601-authentication-ntlm Section 3. "Comment out the proxyAuthenticationProcessingFilter bean:" <bean id="proxyAuthenticationProcessingFilter" class="com.jaspersoft.jasperserver.api.security.EncryptionAuthenticationProcessingFilter" /> --> <!-- ############ PreAuth ############ --> <bean id="proxyRequestParameterAuthenticationFilter" class="com.jaspersoft.jasperserver.war.util.ExternalRequestParameterAuthenticationFilter" parent="requestParameterAuthenticationFilter"> <property name="authenticationManager" ref="ldapAuthenticationManager"/> <property name="externalDataSynchronizer" ref="externalDataSynchronizer"/> </bean> <bean id="externalAuthSuccessHandler" class="com.jaspersoft.jasperserver.api.security.externalAuth.JrsExternalAuthenticationSuccessHandler" parent="successHandler"> <property name="externalDataSynchronizer" ref="externalDataSynchronizer"/> </bean> <bean id="proxyBasicProcessingFilter" class="com.jaspersoft.jasperserver.api.security.externalAuth.ExternalAuthBasicProcessingFilter" parent="basicProcessingFilter"> <constructor-arg index="0"> <ref bean="ldapAuthenticationManager"/> </constructor-arg> <!-- not working from sample file: <property name="authenticationManager" ref="ldapAuthenticationManager"/> --> <property name="externalDataSynchronizer" ref="externalDataSynchronizer"/> </bean> <bean id="ldapAuthenticationManager" class="com.jaspersoft.jasperserver.api.security.externalAuth.wrappers.spring.JSProviderManager"> <constructor-arg index="0"> <list> <ref bean="preAuthProvider"/> <!-- <ref bean="ldapAuthenticationProvider"/> --> <ref bean="${bean.daoAuthenticationProvider}"/> <!--anonymousAuthenticationProvider only needed if filterInvocationInterceptor.alwaysReauthenticate is set to true <ref bean="anonymousAuthenticationProvider"/>--> </list> </constructor-arg> </bean> <!-- <bean id="anonymousAuthenticationProvider" class="org.springframework.security.providers.anonymous.AnonymousAuthenticationProvider"> <property name="key"><value>foobar</value></property> </bean>--> <!-- LDAP-based populator with a groupSearchFilter specific for Microsoft-AD: It returns all groups a user is member of, including cascaded groups Explanation (in German): https://www.faq-o-matic.net/2017/09/06/verschachtelte-ad-gruppen-mit-ldap-filter-abfragen-2/ English example: https://community.jaspersoft.com/questions/1048501/ldap-ad-2012-nested-ad-groups-and-role-mapping This must be adopted for other LDAP-servers! See Auth Cookbook for details (and problems) https://community.jaspersoft.com/documentation/jasperreports-server-authentication-cookbook/v550/overview-external-ldap e.g.: The beans that perform LDAP authentication do not map information such as the user’s full name, email address, or profile attributes that may exist in the LDAP directory. This requires customizing the FilterBasedLdapUserSearch bean, as described in Advanced Topics. --> <bean id="ldapAuthPopulator" class="com.jaspersoft.jasperserver.api.security.externalAuth.wrappers.spring.ldap.JSDefaultLdapAuthoritiesPopulator"> <constructor-arg index="0"> <ref bean="ldapContextSource"/> </constructor-arg> <constructor-arg index="1"> <value></value> </constructor-arg> <property name="groupRoleAttribute" value="cn"/> <!-- Get all (cascaded) groups --> <property name="groupSearchFilter" value="(&(objectCategory=group)(member:1.2.840.113556.1.4.1941:={0}))"/> <property name="searchSubtree" value="true"/> <!-- Seems to be an MS-AD specific --> <property name="ignorePartialResultException" value="true"/> <!-- rolePrefix is deprecated. (at least with JS 7.1.0): Map the authorities in the {@code AuthenticationProvider} using a {@code GrantedAuthoritiesMapper}. Problem of this deprecation: There a JS wrapper class {@code org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider} for the spring class {@code org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider}, but it doesn't offer support for {@code GrantedAuthoritiesMapper} So I left it as it is (and it works right now (JS 7.1.0)) --> <property name="rolePrefix"> <value>ROLE_LDAP_</value> </property> <!-- convertToUpperCase is deprecated. (at least with JS 7.1.0): Convert case in the {@code AuthenticationProvider} using a {@code GrantedAuthoritiesMapper}. --> <property name="convertToUpperCase" value="false"/> <!-- Setting the defaultRole is deprecated. (at least with JS 7.1.0): Assign a default role in the {@code AuthenticationProvider} using a {@code GrantedAuthoritiesMapper}. Just DON'T use it! (With ExternalInetOrgPersonUserSetupProcessor, this role was detected as externally defined, renamed to *_EXT and effectively broken) --> <!-- <property name="defaultRole" value="ROLE_USER"/> --> </bean> <!-- Microsoft-AD specific search by sAMAccountName (similar to https://community.jaspersoft.com/documentation/tibco-jasperreports-server-authentication-cookbook/v601/authentication-microsoft ) --> <bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch"> <!-- <bean id="userSearch" class="de.stueken.security.externalAuth.wrappers.spring.ldap.AttributedFilterBasedLdapUserSearch"> --> <!-- class="com.jaspersoft.jasperserver.api.security.externalAuth.wrappers.spring.ldap.JSFilterBasedLdapUserSearch"> --> <constructor-arg index="0"> <value></value> </constructor-arg> <constructor-arg index="1"> <value>(sAMAccountName={0})</value> </constructor-arg> <constructor-arg index="2"> <ref bean="ldapContextSource" /> </constructor-arg> <property name="searchSubtree"> <value>true</value> </property> <property name="returningAttributes"> <list> <!-- You need at least cn and sn. Without cn, the mapping gives a NullPointerException. Without sn you'll get an IllegalArgumentException: [Assertion failed] in org.springframework.security.ldap.userdetails.Person --> <value>cn</value> <value>sn</value> <!-- displayName and mail is the stuff you are here for --> <!-- Used for JS fullname --> <value>displayName</value> <!-- Used for JS emailAddress --> <value>mail</value> <!-- Now for the optional rest ...--> <value>sAMAccountName</value> <!-- <value>name</value> --> </list> </property> </bean> <!-- Use settings taken form js.externalAuth.properties userDN (like "Technical.User@domain.tld") and password are necessary, if the LDAP doesn't allow anonymous requests. --> <bean id="ldapContextSource" class="com.jaspersoft.jasperserver.api.security.externalAuth.ldap.JSLdapContextSource"> <constructor-arg value="${external.ldap.url}"/> <!-- manager user name and password (may not be needed) --> <property name="userDn" value="${external.ldap.username}"/> <property name="password" value="${external.ldap.password}"/> <property name="referral" value="follow"/> </bean> <!-- ############ LDAP authentication ############ --> <!-- ############ JRS Synchronizer ############ --> <bean id="externalDataSynchronizer" class="com.jaspersoft.jasperserver.api.security.externalAuth.ExternalDataSynchronizerImpl"> <property name="externalUserProcessors"> <list> <!-- Choose externalInetOrgPersonUserSetupProcessor or externalUserSetupProcessor --> <!-- applies displayName as fullname and mail as emailAddress to the jasperserver user, but needs the class ExternalInetOrgPersonUserSetupProcessor --> <ref bean="externalInetOrgPersonUserSetupProcessor"/> <!-- doesn't apply the InetOrgPerson-properties to the jasperserver user, but doesn't need the extra classes --> <!-- <ref bean="externalUserSetupProcessor"/> --> <!-- Example processor for creating user folder--> <!--<ref bean="externalUserFolderProcessor"/>--> </list> </property> </bean> <bean id="abstractExternalProcessor" class="com.jaspersoft.jasperserver.api.security.externalAuth.processors.AbstractExternalUserProcessor" abstract="true"> <property name="repositoryService" ref="${bean.repositoryService}"/> <property name="userAuthorityService" ref="${bean.userAuthorityService}"/> <property name="tenantService" ref="${bean.tenantService}"/> <property name="profileAttributeService" ref="profileAttributeService"/> <property name="objectPermissionService" ref="objectPermissionService"/> </bean> <!-- The ExternalInetOrgPersonUserSetupProcessor applies displayName as fullname and mail as emailAddress to the jasperserver user --> <bean id="externalInetOrgPersonUserSetupProcessor" class="de.stueken.jasperserver.api.security.externalAuth.processors.ExternalInetOrgPersonUserSetupProcessor" parent="abstractExternalProcessor"> <!--Default permitted role characters; others are removed. Change regular expression to allow other chars.--> <property name="permittedExternalRoleNameRegex" value="[A-Za-z0-9_\-]+"/> <property name="userAuthorityService"> <ref bean="${bean.internalUserAuthorityService}"/> </property> <!-- These are the default attribute names 'displayName' and 'mail' Be careful on changes: Value must be an InetOrgPerson String property, it is case-sensitive, and must fit the returningAttributes of the userSearch <property name="fullNameAttribute" value="displayName"/> <property name="emailAddressAttribute" value="mail"/> --> <property name="adminUsernames"> <!--Add yourself or another admins username, if you like. The following defaultAdminRoles must be set!--> <list> <!--<value>Username</value>--> </list> </property> <property name="defaultAdminRoles"> <list> <!-- Don't use ldapAuthPopulator's property defaultRole with the same role! --> <value>ROLE_ADMINISTRATOR</value> </list> </property> <property name="defaultInternalRoles"> <list> <!-- Don't use ldapAuthPopulator's property defaultRole with the same role! --> <value>ROLE_USER</value> </list> </property> <property name="organizationRoleMap"> <map> <!-- Example of mapping customer roles to JRS roles --> <entry> <key> <value>ROLE_ADMIN_EXTERNAL_ORGANIZATION</value> </key> <!-- JRS role that the <key> external role is mapped to--> <value>ROLE_ADMINISTRATOR</value> </entry> </map> </property> </bean> <!-- EXAMPLE Processor <bean id="externalUserFolderProcessor" class="com.jaspersoft.jasperserver.api.security.externalAuth.processors.ExternalUserFolderProcessor" parent="abstractExternalProcessor"> <property name="repositoryService" ref="${bean.unsecureRepositoryService}"/> </bean> --> <!-- ############ JRS Synchronizer ############ --> <!-- ############ Useless RequestHeaderAuthenticationFilter ############ --> <!-- Form e.g. https://community.jaspersoft.com/questions/851215/container-security-community-server-601 RequestHeaderAuthenticationFilter is useless, if you need the externalDataSynchronizer, because it's inherited from org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter See comment in com.jaspersoft.jasperserver.api.security.externalAuth.preauth.BasePreAuthenticatedProcessingFilter * This filter class extends AbstractPreAuthenticatedProcessingFilter to * call externalDataSynchronizer on successful authentication. --> <!-- This filter will be added to the wildcard filter chain to intercept the request headers and pass them to the authentication manager. This filter must not throw an exception to allow other filters to show a login screen The name of the principalRequestHeader field must fit the name set in the Apache configuration: RequestHeader set X-Remote-User expr=%{REMOTE_USER} --> <!-- <bean id="proxyPreAuthenticatedProcessingFilter_SPRING" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter"> <property name="principalRequestHeader" value="X-Remote-User"/> <property name="exceptionIfHeaderMissing" value="false"/> <property name="authenticationManager" ref="ldapAuthenticationManager"/> </bean>--> <!-- ############ Useless RequestHeaderAuthenticationFilter ############ --> </beans>
Change for debugging tests without fullname and email
If you like to test it first without the mapping of emailAddress and fullname, just go to the externalDataSynchronizer bean and enable externalUserSetupProcessor instead of externalInetOrgPersonUserSetupProcessor, like:
<!-- applies displayName as fullname and mail as emailAddress to the JasperReports Server user, but needs the class ExternalInetOrgPersonUserSetupProcessor --> <!-- <ref local="externalInetOrgPersonUserSetupProcessor"/> --> <!-- doesn't apply the InetOrgPerson-properties to the JasperReports Server user, but doesn't need the extra classes --> <ref local="externalUserSetupProcessor"/>
You must comment out the bean definition of ExternalInetOrgPersonUserSetupProcessor, if it's not available in the classpath!
Enable at least INFO-logging of com.jaspersoft.jasperserver.api.security in WEB-INF/log4j.properties:
# Security Logging log4j.logger.com.jaspersoft.jasperserver.api.security=INFO
Map displayName to fullname and mail to emailAddress
Therefore, you need to compile the following Java-class, put it in a jar (mine is called jasperserverX-externalUserSetup.1.0.jar) and place it in the jasperserver/WEB-INF/lib
You might want to throw out the reflection stuff and make it more hard-coded, if you like.
/** * */ package de.stueken.jasperserver.api.security.externalAuth.processors; import java.lang.reflect.Field; /** commons-logging-1.1.1.jar */ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** spring-security-core-3.2.10.RELEASE.jar */ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; /** spring-security-ldap-3.2.10.RELEASE-sources.jar */ import org.springframework.security.ldap.userdetails.InetOrgPerson; import org.springframework.util.Assert; /** jasperserver-api-metadata-7.1.0.jar * jasperserver-api-metadata-impl-7.1.0.jar */ import com.jaspersoft.jasperserver.api.metadata.user.domain.User; /** jasperserver-api-externalAuth-7.1.0.jar * jasperserver-api-externalAuth-impl-7.1.0.jar */ import com.jaspersoft.jasperserver.api.security.externalAuth.processors.ExternalUserSetupProcessor; /** * An {@link InetOrgPerson} has a {@link InetOrgPerson#getDisplayName()} as the full name and a * {@link InetOrgPerson#getMail()} as email address. This processor grabs both properties from the * {@link Authentication} object and applies them to the {@link User}. * * @version 1.0 * @author Peter F. / stueken.de * @copyright 2018 * */ public class ExternalInetOrgPersonUserSetupProcessor extends ExternalUserSetupProcessor { private static final Log logger = LogFactory .getLog(ExternalInetOrgPersonUserSetupProcessor.class); private String fullNameAttribute = "displayName"; //$NON-NLS-1$ private String emailAddressAttribute = "mail"; //$NON-NLS-1$ /** * Updates the {@link User} user with full name and email address from the {@link InetOrgPerson} * principal. If no display name is provided for the principal, the username is used as full name. * * @see com.jaspersoft.jasperserver.api.security.externalAuth.processors.ExternalUserSetupProcessor# * updateUserAttributes(com.jaspersoft.jasperserver.api.metadata.user.domain.User) */ @SuppressWarnings("nls") @Override protected void updateUserAttributes(User user) { Assert.notNull(user, "User must be provided!"); super.updateUserAttributes(user); // Get principal as InetOrgPerson InetOrgPerson person = getInetOrgPerson(); if (person != null) { // Fallback to Username /* * 'Hardcoded' version: String fullName = person.getDisplayName(); */ String fullName = getString(person, getFullNameAttribute()); if (fullName == null) fullName = person.getUsername(); if ((user.getFullName() == null) || !user.getFullName().equalsIgnoreCase(fullName)) { if (logger.isInfoEnabled()) logger.info("User '" + user.getUsername() + "' got new fullname: '" + fullName + "', was '" + user.getFullName() + "'"); user.setFullName(fullName); } // get email /* * 'Hardcoded' version: String emailAddress = person.getMail(); */ String emailAddress = getString(person, getEmailAddressAttribute()); if ((user.getEmailAddress() == null) || !user.getEmailAddress().equalsIgnoreCase(emailAddress)) { if (logger.isInfoEnabled()) logger.info("User '" + user.getUsername() + "' got new emailAddress: '" + emailAddress + "', was '" + user.getEmailAddress() + "'"); user.setEmailAddress(emailAddress); } } if (logger.isInfoEnabled()) logger.info("Check UserAttributes for '" + user.getUsername() + "': " + user.getFullName() + " <" + user.getEmailAddress() + ">"); } /** * * @return principal as InetOrgPerson or <code>null</code>, if no Authentication or no Principal * (as InetOrgPerson) */ protected InetOrgPerson getInetOrgPerson() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { logger.debug("Authentication of " + auth.getClass()); //$NON-NLS-1$ Object principal = auth.getPrincipal(); if ((principal != null) && logger.isDebugEnabled()) logger.debug("Principal of " + principal.getClass()); //$NON-NLS-1$ if (principal instanceof InetOrgPerson) return (InetOrgPerson) principal; } return null; } /* * ---------------------------------------------------------------------------------------------- * The following stuff makes it more flexible, but you might prefer the short 'hardcoded' version * ---------------------------------------------------------------------------------------------- */ /** * @return the fullNameAttribute */ public String getFullNameAttribute() { return fullNameAttribute; } /** * The name of the {@link InetOrgPerson} property (an LDAP attribute) to use for the * {@link User#fullName}. Defaults to "displayName" * * @param fullNameAttribute the fullNameAttribute to set */ @SuppressWarnings("javadoc") public void setFullNameAttribute(String fullNameAttribute) { if (fullNameAttribute != null) this.fullNameAttribute = fullNameAttribute; } /** * @return the emailAddressAttribute */ public String getEmailAddressAttribute() { return emailAddressAttribute; } /** * The name of the {@link InetOrgPerson} property (an LDAP attribute) to use for the * {@link User#emailAddress} Defaults to "mail" * * @param emailAddressAttribute the emailAddressAttribute to set */ @SuppressWarnings("javadoc") public void setEmailAddressAttribute(String emailAddressAttribute) { if (emailAddressAttribute != null) this.emailAddressAttribute = emailAddressAttribute; } /** * Generic (String) getter from * https://stackoverflow.com/questions/14374878/using-reflection-to-set-an-object-property * * @param object Java object * @param fieldName property * @return result of the getter */ @SuppressWarnings({ "nls" }) protected static String getString(Object object, String fieldName) { Class<?> clazz = object.getClass(); while (clazz != null) { try { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); return (String) field.get(object); } catch (NoSuchFieldException e) { if (logger.isDebugEnabled()) logger .debug("Check your bean configuration! NoSuchFieldException with getter of property '" + fieldName + "' of object of " + clazz); clazz = clazz.getSuperclass(); } catch (Exception e) { if (logger.isDebugEnabled()) logger .debug("Exception with getter of property '" + fieldName + "' of object of " + clazz); logger.debug(e.getStackTrace()); clazz = clazz.getSuperclass(); } } return null; } }
Prepare for JasperServer updates
Save the following files to another directory. In case of an update of the JasperServer, you copy the "To copy" files into your new installation and apply the settings from the "As reference" files.
To copy
WEB-INF/applicationContext-externalAuth-LDAP.xml
WEB-INF/lib/jasperserverX-externalUserSetup.1.0.jar
As reference
WEB-INF/js.config.properties
WEB-INF/log4j.properties
Strange jasperserver.log entries you can ignore (or almost, AFAIK)
Logfile-entry: Current system user can't modify keystore.
You see entries similar to this one in the jasperserver.log
- WARN KeystoreManager(Initialization),localhost-startStop-1:54 - Current system user can't modify keystore.
Solution
- Allow the group tomcat8 to access the key-files of the system user you use:
sudo chgrp tomcat8 ~/.jrsks*
Logfile-entry: OperationNotSupportedException: Context is read only
You see entries similar to this one in the jasperserver.log
- ERROR JNDIResourceProvider,localhost-startStop-1:75 - error closing context javax.naming.OperationNotSupportedException: Context is read only
This seems to be a very old bug and most probably you can just ignore it
- See https://community.jaspersoft.com/questions/822153/error-closing-context
- Posted on December 10, 2013 at 7:39pm
- For some people the suggested solution seems to work. I tried it without success
- Latest Comment from March 2021 (in Spanish): The issue still exists with JasperServer 7.8.0 Community Edition
Known issues
Session Timeout should be increased
By default, the session timeout is set to 20 minutes in jasperserver/WEB-INF/web.xml. Increase this value to something fitting your environment, e.g. 8 hours
<session-config> <!-- Default to 20 minute session timeouts --> <!-- Changed to 8h = 480min to prevent strange user experience --> <session-timeout>480</session-timeout> </session-config>
See e.g. https://community.jaspersoft.com/wiki/how-configure-session-timeout-jasperreports-server
- Explanation
- If a user stays inactive in JasperServer for a longer time than defined in the session-timeout, the session will be invalidated and the user must login again (JasperServer default behavior)
- If the user
- has an open report in the browser and
- just changes the options after the session timed out and
- press 'Apply'
- => Since the session is invalid, the report reloads including a Kerberos-Login, and the options are reset
- This results in a strange experience for the users and should be circumvented by setting the session timeout value to e.g. 8h (the daily working hours). That is, the user will login automatically by Kerberos on every morning, but the session will remain active during the day.
Test with a JasperServer installation with all internal components
Note 2022-10-27: Not checked with update!
I strongly suggest to set up an test VM with a JasperServer using internal components only! Then you can focus on getting the user authentication run.
Problem with OpenJDK 11 (vs. OpenJDK 8) when using system Tomcat 8
Note 2022-10-27: Not checked with update!
With OpenJDK 11 installed, JasperServer is unable to start. This seems to be related to an old hibernate-validator-5.2.2.Final.jar, which throws an exception on checking the Java version with OpenJDK 11.
I haven't checked, if this is the only jar incompatible with OpenJDK 11, but would use OpenJDK 8 exclusively for the time being.
Access with JasperServer Studio
Note 2022-10-27: Not checked with update!
AFAIK, the Kerberos-authenticated access doesn't work with JasperStudio, neither the LDAP-based access with externally defined users (might differ in other environments).
Using an internal user via Apache gives me no access, so IMHO the only viable solution seems to be to access Tomcat on port 8080 directly with an internal user.
From a remote host, this will only work if you remove the IP-restriction on this port, so remove or change the 'address="127.0.0.1"'-attribute:
Change conf/server.xml in <Service name="Catalina">
<Connector port="8080" URIEncoding="UTF-8" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> <!-- removed: address="127.0.0.1" -->
Then use the URL http://jasperserver.domain.com:8080/jasperserver/services/repository in the Studio.
See https://community.jaspersoft.com/wiki/connect-jaspersoft-studio-jasperreports-server for more information.
Open
Note 2022-10-27: No plans to work on this in the foreseeable future
- How do I install the nice Tomcat ROOT webapp with the index.jsp as provided by JasperReports Server with the system package manager (in only installs the index.html ROOT version)?
- How do I easily allow certain LDAP-users access to the Tomcat-Management (instead of using static users in conf/tomcat-users.xml)?
Recommended Comments
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now