├── .gitignore ├── LICENSE ├── README.md ├── docs └── images │ ├── jwt-config.png │ ├── postman-auth.png │ └── postman-result.png ├── examples └── docker │ └── tomcat │ ├── Dockerfile │ ├── custom-jwt-validator │ ├── .gitignore │ ├── pom.xml │ └── src │ │ └── main │ │ └── groovy │ │ └── io │ │ └── digitalstate │ │ └── camunda │ │ └── custom │ │ └── jwt │ │ └── ValidatorJwt.groovy │ ├── docker-compose.yml │ └── docker │ ├── camunda │ ├── conf │ │ └── bpm-platform.xml │ └── webapps │ │ └── engine-rest │ │ └── WEB-INF │ │ ├── lib │ │ ├── camunda-rest-api-jwt-authentication-v0.5-jar-with-dependencies.jar │ │ └── example-custom-jwt-validator-v1.0.jar │ │ └── web.xml │ └── keys │ └── key.pub ├── pom.xml └── src └── main └── groovy └── io └── digitalstate └── camunda └── authentication └── jwt ├── AbstractValidatorJwt.groovy ├── AuthenticationFilterJwt.groovy ├── AuthenticationProviderJwt.groovy ├── AuthenticationResultJwt.groovy ├── ProcessEngineAuthenticationFilterJwt.groovy └── ValidatorResultJwt.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | .idea 11 | *.iml 12 | /target 13 | 14 | # Mobile Tools for Java (J2ME) 15 | .mtj.tmp/ 16 | 17 | # Package Files # 18 | *.jar 19 | *.war 20 | *.nar 21 | *.ear 22 | *.zip 23 | *.tar.gz 24 | *.rar 25 | 26 | !/examples/**/*.jar 27 | 28 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 29 | hs_err_pid* 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 DigitalState 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Camunda JWT REST API Validation Provider 2 | 3 | Provides a JWT Validation provider for Camunda BPM's REST API. 4 | 5 | This JWT provider is designed only to validate JWT tokens and inject the Username, Group IDs, and Tenant IDs 6 | that the username is part of. The provider does **not** issue JWT tokens during the login process. 7 | 8 | Groups in Camunda are used for controlling the access. You will need to define groups with accesses. 9 | If you are working with Camunda as a generic microservice and do not need accesses, other than the master 10 | user/admin user, then use the group `camunda-admin`, which is the default admin group. 11 | 12 | # How it works 13 | 14 | This provider is added into the Camunda's engine-rest application which is the Rest API for Camunda BPM. 15 | 16 | A Servlet Filter is added into the engine-rest app which will process requests in the JWT Authentication provider. 17 | 18 | Two initialization parameters of the filter are provided for easy customization: 19 | 20 | 1. `jwt-secret-path` : The file path to a file containing the JWT secret used to decode/validate the JWT. The value can be null if you get your secret from a different source. 21 | 1. `jwt-validator` : The fully qualified class name of the class that will validate the JWT. 22 | 23 | It is expected that the JWT is using the standard `Authorization` header with the format `Bearer theJwtTokenHere` 24 | 25 | 26 | # Included JWT Validation Library 27 | 28 | By default the java-jwt library is included: https://github.com/auth0/java-jwt 29 | You can use this library to in your Validator class. 30 | 31 | 32 | # Screenshots of usage 33 | 34 | Postman Result for a valid JWT but the user does not have the proper group authorizations for access: 35 | 36 | ![Postman result](./docs/images/postman-result.png) 37 | 38 | 39 | JWT Config: 40 | ![JWT Config](./docs/images/jwt-config.png) 41 | 42 | 43 | 44 | # Example usages 45 | 46 | # Tomcat Docker 47 | 48 | 1. Set your password/key/secret in the `./examples/docker/tomcat/docker/keys/key.pub` file. 49 | 1. In Terminal, go to: `examples/docker/tomcat`, and run `docker-compose up` 50 | 1. Go to `localhost:8055/camunda` and create a admin user 51 | 1. Go to jwt.io and use the "Debugger" to create a JWT token: The payload should look like 52 | ```json 53 | { 54 | "sub": "...", 55 | "username": "admin", 56 | "groupIds": ["camunda-admin"], 57 | "tenantIds": [], 58 | "iat": .... 59 | } 60 | ``` 61 | Lead the sub and iat values as their default values that are provided by jwt.io. 62 | 1. set the secret on the bottom right of the jwt.io page to the secret that is in the key.pub file that you set in the previous step. 63 | 1. Copy the encoded token on the left of the jwt.io page 64 | 1. In Postman (getpostman.com) in your request to the camunda api, go to the Authentication tab and paste the copied token in the Bearer token field: 65 | ![token](./docs/images/postman-auth.png) 66 | 1. Execute the request. 67 | 68 | If you want to see what happens when you get a access denied, change remove the admin group from the `groupIds` field 69 | 70 | # How to install 71 | 72 | If you are building a Spring Boot or generating a JAR wth camunda, you can install the library as follows: 73 | 74 | Add JitPack as a repository source in your build file. 75 | 76 | If you are using Maven, then add the following to your pom.xml 77 | 78 | ```xml 79 | 80 | ... 81 | 82 | 83 | jitpack.io 84 | https://jitpack.io 85 | 86 | 87 | ... 88 | ``` 89 | 90 | This snippet will enable Maven dependency download directly from Github.com 91 | 92 | Then add the following dependency: 93 | 94 | ```xml 95 | ... 96 | 97 | com.github.digitalstate 98 | camunda-rest-jwt-authentication 99 | v0.5 100 | compile 101 | 102 | ``` 103 | 104 | If you are using a existing Docker Container with Camunda / the Shared Engine configuration of Camunda, and you would like to add JWT see: 105 | 106 | 1. Tomcat: [`./examples/docker/tomcat`](./examples/docker/tomcat) 107 | 108 | 109 | # Tomcat Servlet Filter Configuration 110 | 111 | Using the JWT filter with Tomcat is added into the web.xml of the engine-rest app. 112 | 113 | If you are using the Shared engine and working with the default docker container of Camunda BPM tomcat, you can override the container such as: 114 | 115 | Dockerfile: 116 | 117 | ```dockerfile 118 | FROM camunda/camunda-bpm-platform:tomcat-7.9.0 119 | 120 | RUN rm -r webapps/camunda-invoice 121 | 122 | COPY docker/camunda/conf/bpm-platform.xml /camunda/conf/bpm-platform.xml 123 | 124 | COPY docker/camunda/webapps/engine-rest/WEB-INF/lib/camunda-rest-api-jwt-authentication-v0.5-jar-with-dependencies.jar /camunda/webapps/engine-rest/WEB-INF/lib/camunda-rest-api-jwt-authentication-v0.5-jar-with-dependencies.jar 125 | COPY docker/camunda/webapps/engine-rest/WEB-INF/web.xml /camunda/webapps/engine-rest/WEB-INF/web.xml 126 | 127 | # Example usage of a standalone jar with the jwt validator class that is extended from the camunda-rest-api-jwt-authentication jar. 128 | COPY docker/camunda/webapps/engine-rest/WEB-INF/lib/example-custom-jwt-validator-v1.0.jar /camunda/webapps/engine-rest/WEB-INF/lib/example-custom-jwt-validator-v1.0.jar 129 | COPY docker/keys/key.pub /keys/key.pub 130 | ``` 131 | 132 | :exclamation: In practice the key.pub file would be stored in a Docker Volume 133 | 134 | In the web.xml file, the following servlet filter is added: 135 | 136 | ```xml 137 | ... 138 | 139 | camunda-jwt-auth 140 | 141 | io.digitalstate.camunda.authentication.jwt.ProcessEngineAuthenticationFilterJwt 142 | 143 | true 144 | 145 | authentication-provider 146 | io.digitalstate.camunda.authentication.jwt.AuthenticationFilterJwt 147 | 148 | 149 | jwt-secret-path 150 | 151 | /keys/key.pub 152 | 153 | 154 | jwt-validator 155 | 156 | io.digitalstate.camunda.custom.jwt.ValidatorJwt 157 | 158 | 159 | 160 | camunda-jwt-auth 161 | /* 162 | 163 | ... 164 | 165 | ``` 166 | 167 | 168 | # Camunda Spring Boot Servlet Filter Configuration 169 | 170 | Using the JWT filter with Spring boot, is very easy to implement. 171 | 172 | Implement your Validator class as you would, and add and configured the following `@Configuration` class into your Spring Boot application: 173 | 174 | ```java 175 | @Configuration 176 | public class CamundaSecurityFilter { 177 | 178 | @Value('${camunda.rest-api.jwt.secret-path}') 179 | String jwtSecretPath; 180 | 181 | @Value('${camunda.rest-api.jwt.validator-class}') 182 | String jwtValidatorClass; 183 | 184 | @Bean 185 | public FilterRegistrationBean processEngineAuthenticationFilter() { 186 | FilterRegistrationBean registration = new FilterRegistrationBean(); 187 | registration.setName("camunda-jwt-auth"); 188 | registration.addInitParameter('authentication-provider', 'io.digitalstate.camunda.authentication.jwt.AuthenticationFilterJwt'); 189 | registration.addInitParameter('jwt-secret-path', jwtSecretPath); 190 | registration.addInitParameter('jwt-validator', jwtValidatorClass); 191 | registration.addUrlPatterns("/rest/*"); 192 | registration.setFilter(getProcessEngineAuthenticationFilter()); 193 | return registration; 194 | } 195 | 196 | @Bean 197 | public Filter getProcessEngineAuthenticationFilter() { 198 | return new ProcessEngineAuthenticationFilterJwt(); 199 | } 200 | } 201 | ``` 202 | 203 | In this example we are using Spring external configuration to get the values of the secret Path and the Validator Class name. 204 | 205 | Feel free to reconfigure the usage as you see fit. 206 | 207 | 208 | # Validation Class Example 209 | 210 | This is a example of a groovy based Validator class that would used to validate a JWT. 211 | 212 | ```groovy 213 | package io.digitalstate.camunda.custom.jwt 214 | 215 | import com.auth0.jwt.JWT 216 | import com.auth0.jwt.JWTVerifier 217 | import com.auth0.jwt.algorithms.Algorithm 218 | import com.auth0.jwt.exceptions.JWTVerificationException 219 | import com.auth0.jwt.interfaces.DecodedJWT 220 | import io.digitalstate.camunda.authentication.jwt.AbstractValidatorJwt 221 | import io.digitalstate.camunda.authentication.jwt.ValidatorResultJwt 222 | import org.slf4j.Logger 223 | import org.slf4j.LoggerFactory 224 | import groovy.transform.CompileStatic 225 | 226 | @CompileStatic 227 | public class ValidatorJwt extends AbstractValidatorJwt { 228 | 229 | private static final Logger LOG = LoggerFactory.getLogger(ValidatorJwt.class) 230 | private static String jwtSecret 231 | 232 | @Override 233 | ValidatorResultJwt validateJwt(String encodedCredentials, String jwtSecretPath) { 234 | if (!jwtSecret){ 235 | try { 236 | jwtSecret = new FileInputStream(jwtSecretPath).getText() 237 | } catch(all){ 238 | LOG.error("ERROR: Unable to load JWT Secret: ${all.getLocalizedMessage()}") 239 | return ValidatorResultJwt.setValidatorResult(false, null, null, null) 240 | } 241 | } 242 | 243 | try { 244 | Algorithm algorithm = Algorithm.HMAC256(jwtSecret); 245 | JWTVerifier verifier = JWT.require(algorithm) 246 | .acceptNotBefore(new Date().getTime()) 247 | .build(); 248 | DecodedJWT jwt = verifier.verify(encodedCredentials) 249 | 250 | String username = jwt.getClaim('username').asString() 251 | List groupIds = jwt.getClaim('groupIds').asList(String) 252 | List tenantIds = jwt.getClaim('tenantIds').asList(String) 253 | 254 | if (!username){ 255 | LOG.error("BAD JWT: Missing username") 256 | return ValidatorResultJwt.setValidatorResult(false, null, null, null) 257 | } 258 | 259 | return ValidatorResultJwt.setValidatorResult(true, username, groupIds, tenantIds) 260 | 261 | } catch(JWTVerificationException exception){ 262 | LOG.error("BAD JWT: ${exception.getLocalizedMessage()}") 263 | return ValidatorResultJwt.setValidatorResult(false, null, null, null) 264 | } 265 | } 266 | 267 | } 268 | ``` 269 | 270 | -------------------------------------------------------------------------------- /docs/images/jwt-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalState/camunda-rest-jwt-authentication/523737d85519a3aab5e7dfddb22b63b51411c2e5/docs/images/jwt-config.png -------------------------------------------------------------------------------- /docs/images/postman-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalState/camunda-rest-jwt-authentication/523737d85519a3aab5e7dfddb22b63b51411c2e5/docs/images/postman-auth.png -------------------------------------------------------------------------------- /docs/images/postman-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalState/camunda-rest-jwt-authentication/523737d85519a3aab5e7dfddb22b63b51411c2e5/docs/images/postman-result.png -------------------------------------------------------------------------------- /examples/docker/tomcat/Dockerfile: -------------------------------------------------------------------------------- 1 | #FROM digitalstate/camunda-bpm-platform:tomcat-7.9.0-json-logging 2 | FROM camunda/camunda-bpm-platform:tomcat-7.9.0 3 | 4 | RUN rm -r webapps/camunda-invoice 5 | 6 | COPY docker/camunda/conf/bpm-platform.xml /camunda/conf/bpm-platform.xml 7 | 8 | COPY docker/camunda/webapps/engine-rest/WEB-INF/lib/camunda-rest-api-jwt-authentication-v0.5-jar-with-dependencies.jar /camunda/webapps/engine-rest/WEB-INF/lib/camunda-rest-api-jwt-authentication-v0.5-jar-with-dependencies.jar 9 | COPY docker/camunda/webapps/engine-rest/WEB-INF/web.xml /camunda/webapps/engine-rest/WEB-INF/web.xml 10 | 11 | # Example usage of a standalone jar with the jwt validator class that is extended from the camunda-rest-api-jwt-authentication jar. 12 | COPY docker/camunda/webapps/engine-rest/WEB-INF/lib/example-custom-jwt-validator-v1.0.jar /camunda/webapps/engine-rest/WEB-INF/lib/example-custom-jwt-validator-v1.0.jar 13 | COPY docker/keys/key.pub /keys/key.pub -------------------------------------------------------------------------------- /examples/docker/tomcat/custom-jwt-validator/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | .idea 11 | *.iml 12 | /target 13 | 14 | 15 | # Mobile Tools for Java (J2ME) 16 | .mtj.tmp/ 17 | 18 | # Package Files # 19 | *.jar 20 | *.war 21 | *.nar 22 | *.ear 23 | *.zip 24 | *.tar.gz 25 | *.rar 26 | 27 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 28 | hs_err_pid* 29 | -------------------------------------------------------------------------------- /examples/docker/tomcat/custom-jwt-validator/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.digitalstate.camunda.custom.jwt 7 | example-custom-jwt-validator 8 | v1.0 9 | jar 10 | 11 | Custom JWT Validator for Camunda BPM 12 | Example of Custom JWT Validator 13 | 14 | 15 | UTF-8 16 | UTF-8 17 | 1.8 18 | 7.9.0 19 | 20 | 21 | 22 | 23 | jitpack.io 24 | https://jitpack.io 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | org.camunda.bpm 33 | camunda-bom 34 | ${camunda.version} 35 | import 36 | pom 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.codehaus.groovy 44 | groovy 45 | 2.4.15 46 | provided 47 | 48 | 49 | 50 | 51 | org.camunda.bpm 52 | camunda-engine 53 | provided 54 | 55 | 56 | 57 | org.slf4j 58 | slf4j-jcl 59 | 1.7.5 60 | provided 61 | 62 | 63 | 64 | com.auth0 65 | java-jwt 66 | 3.4.0 67 | provided 68 | 69 | 70 | 71 | javax.servlet 72 | javax.servlet-api 73 | 3.1.0 74 | provided 75 | 76 | 77 | 78 | javax.ws.rs 79 | javax.ws.rs-api 80 | 2.0 81 | provided 82 | 83 | 84 | 85 | com.github.digitalstate 86 | camunda-rest-jwt-authentication 87 | 54c9a7e6ae 88 | 89 | 90 | provided 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | org.codehaus.gmavenplus 99 | gmavenplus-plugin 100 | 1.5 101 | 102 | 1.8 103 | 104 | 105 | 106 | 107 | addSources 108 | addTestSources 109 | generateStubs 110 | compile 111 | testGenerateStubs 112 | testCompile 113 | removeStubs 114 | removeTestStubs 115 | 116 | 117 | 118 | 119 | 120 | maven-assembly-plugin 121 | 2.2.1 122 | 123 | 124 | jar-with-dependencies 125 | 126 | 127 | 128 | 129 | make-assembly 130 | package 131 | 132 | single 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /examples/docker/tomcat/custom-jwt-validator/src/main/groovy/io/digitalstate/camunda/custom/jwt/ValidatorJwt.groovy: -------------------------------------------------------------------------------- 1 | package io.digitalstate.camunda.custom.jwt 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.JWTVerifier 5 | import com.auth0.jwt.algorithms.Algorithm 6 | import com.auth0.jwt.exceptions.JWTVerificationException 7 | import com.auth0.jwt.interfaces.DecodedJWT 8 | import io.digitalstate.camunda.authentication.jwt.AbstractValidatorJwt 9 | import io.digitalstate.camunda.authentication.jwt.ValidatorResultJwt 10 | import org.slf4j.Logger 11 | import org.slf4j.LoggerFactory 12 | import groovy.transform.CompileStatic 13 | 14 | @CompileStatic 15 | public class ValidatorJwt extends AbstractValidatorJwt { 16 | 17 | private static final Logger LOG = LoggerFactory.getLogger(ValidatorJwt.class) 18 | private static String jwtSecret 19 | 20 | @Override 21 | ValidatorResultJwt validateJwt(String encodedCredentials, String jwtSecretPath) { 22 | if (!jwtSecret){ 23 | try { 24 | jwtSecret = new FileInputStream(jwtSecretPath).getText() 25 | } catch(all){ 26 | LOG.error("ERROR: Unable to load JWT Secret: ${all.getLocalizedMessage()}") 27 | return ValidatorResultJwt.setValidatorResult(false, null, null, null) 28 | } 29 | } 30 | 31 | try { 32 | Algorithm algorithm = Algorithm.HMAC256(jwtSecret); 33 | JWTVerifier verifier = JWT.require(algorithm) 34 | .acceptNotBefore(new Date().getTime()) 35 | .build(); 36 | DecodedJWT jwt = verifier.verify(encodedCredentials) 37 | 38 | String username = jwt.getClaim('username').asString() 39 | List groupIds = jwt.getClaim('groupIds').asList(String) 40 | List tenantIds = jwt.getClaim('tenantIds').asList(String) 41 | 42 | if (!username){ 43 | LOG.error("BAD JWT: Missing username") 44 | return ValidatorResultJwt.setValidatorResult(false, null, null, null) 45 | } 46 | 47 | return ValidatorResultJwt.setValidatorResult(true, username, groupIds, tenantIds) 48 | 49 | } catch(JWTVerificationException exception){ 50 | LOG.error("BAD JWT: ${exception.getLocalizedMessage()}") 51 | return ValidatorResultJwt.setValidatorResult(false, null, null, null) 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /examples/docker/tomcat/docker-compose.yml: -------------------------------------------------------------------------------- 1 | camunda: 2 | build: . 3 | environment: 4 | - JAVA_OPTS=-Djava.security.egd=file:/dev/./urandom -Duser.timezone=America/Montreal 5 | - PRETTY_JSON_LOG=true 6 | ports: 7 | - "8055:8080" 8 | -------------------------------------------------------------------------------- /examples/docker/tomcat/docker/camunda/conf/bpm-platform.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | default 11 | org.camunda.bpm.engine.impl.cfg.StandaloneProcessEngineConfiguration 12 | java:jdbc/ProcessEngine 13 | 14 | 15 | full 16 | true 17 | true 18 | true 19 | 00:01 20 | 21 | 22 | 23 | 24 | 25 | org.camunda.bpm.application.impl.event.ProcessApplicationEventListenerPlugin 26 | 27 | 28 | 29 | 30 | org.camunda.spin.plugin.impl.SpinProcessEnginePlugin 31 | 32 | 33 | 34 | 35 | org.camunda.connect.plugin.impl.ConnectProcessEnginePlugin 36 | 37 | 38 | 39 | 40 | 41 | 42 | 74 | 75 | 76 | 77 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/docker/tomcat/docker/camunda/webapps/engine-rest/WEB-INF/lib/camunda-rest-api-jwt-authentication-v0.5-jar-with-dependencies.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalState/camunda-rest-jwt-authentication/523737d85519a3aab5e7dfddb22b63b51411c2e5/examples/docker/tomcat/docker/camunda/webapps/engine-rest/WEB-INF/lib/camunda-rest-api-jwt-authentication-v0.5-jar-with-dependencies.jar -------------------------------------------------------------------------------- /examples/docker/tomcat/docker/camunda/webapps/engine-rest/WEB-INF/lib/example-custom-jwt-validator-v1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DigitalState/camunda-rest-jwt-authentication/523737d85519a3aab5e7dfddb22b63b51411c2e5/examples/docker/tomcat/docker/camunda/webapps/engine-rest/WEB-INF/lib/example-custom-jwt-validator-v1.0.jar -------------------------------------------------------------------------------- /examples/docker/tomcat/docker/camunda/webapps/engine-rest/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | org.camunda.bpm.engine.rest.impl.FetchAndLockContextListener 7 | 8 | 9 | 10 | EmptyBodyFilter 11 | org.camunda.bpm.engine.rest.filter.EmptyBodyFilter 12 | true 13 | 14 | 15 | EmptyBodyFilter 16 | /* 17 | 18 | 19 | 20 | CacheControlFilter 21 | org.camunda.bpm.engine.rest.filter.CacheControlFilter 22 | true 23 | 24 | 25 | CacheControlFilter 26 | /* 27 | 28 | 29 | 30 | 49 | 50 | 51 | 52 | camunda-jwt-auth 53 | 54 | io.digitalstate.camunda.authentication.jwt.ProcessEngineAuthenticationFilterJwt 55 | 56 | true 57 | 58 | authentication-provider 59 | io.digitalstate.camunda.authentication.jwt.AuthenticationFilterJwt 60 | 61 | 62 | jwt-secret-path 63 | 64 | /keys/key.pub 65 | 66 | 67 | jwt-validator 68 | 69 | io.digitalstate.camunda.custom.jwt.ValidatorJwt 70 | 71 | 72 | 73 | camunda-jwt-auth 74 | /* 75 | 76 | 77 | 78 | Resteasy 79 | org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher 80 | 81 | javax.ws.rs.Application 82 | org.camunda.bpm.engine.rest.impl.application.DefaultApplication 83 | 84 | true 85 | 86 | 87 | 88 | Resteasy 89 | /* 90 | 91 | 92 | -------------------------------------------------------------------------------- /examples/docker/tomcat/docker/keys/key.pub: -------------------------------------------------------------------------------- 1 | password123 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.digitalstate.camunda.authentication.jwt 7 | camunda-rest-api-jwt-authentication 8 | v0.5 9 | jar 10 | 11 | Camunda REST API JWT Authentication Provider 12 | JWT Authentication Provider for Camunda BPM's REST API 13 | 14 | 15 | UTF-8 16 | UTF-8 17 | 1.8 18 | 7.9.0 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.camunda.bpm 26 | camunda-bom 27 | ${camunda.version} 28 | import 29 | pom 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.codehaus.groovy 37 | groovy 38 | 2.4.15 39 | 40 | 41 | 42 | 43 | org.camunda.bpm 44 | camunda-engine 45 | provided 46 | 47 | 48 | 49 | org.slf4j 50 | slf4j-jcl 51 | 1.7.5 52 | provided 53 | 54 | 55 | 56 | com.auth0 57 | java-jwt 58 | 3.4.0 59 | compile 60 | 61 | 62 | 63 | javax.servlet 64 | javax.servlet-api 65 | 3.1.0 66 | provided 67 | 68 | 69 | 70 | javax.ws.rs 71 | javax.ws.rs-api 72 | 2.0 73 | provided 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.codehaus.gmavenplus 82 | gmavenplus-plugin 83 | 1.5 84 | 85 | 1.8 86 | 87 | 88 | 89 | 90 | addSources 91 | addTestSources 92 | generateStubs 93 | compile 94 | testGenerateStubs 95 | testCompile 96 | removeStubs 97 | removeTestStubs 98 | 99 | 100 | 101 | 102 | 103 | maven-assembly-plugin 104 | 2.2.1 105 | 106 | 107 | jar-with-dependencies 108 | 109 | 110 | 111 | 112 | make-assembly 113 | package 114 | 115 | single 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/main/groovy/io/digitalstate/camunda/authentication/jwt/AbstractValidatorJwt.groovy: -------------------------------------------------------------------------------- 1 | package io.digitalstate.camunda.authentication.jwt 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | abstract class AbstractValidatorJwt { 7 | // @TODO Add use of logger 8 | 9 | abstract ValidatorResultJwt validateJwt(String encodedCredentials, String jwtSecretPath) 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/io/digitalstate/camunda/authentication/jwt/AuthenticationFilterJwt.groovy: -------------------------------------------------------------------------------- 1 | package io.digitalstate.camunda.authentication.jwt 2 | 3 | import groovy.transform.CompileStatic 4 | import org.camunda.bpm.engine.ProcessEngine 5 | import org.slf4j.Logger 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import javax.ws.rs.core.HttpHeaders; 11 | 12 | /** 13 | *

14 | * Authenticates a request against the JWT Token and JWT Validator. A successful validation will generate a ValidatorResultJwt object which is used to inject the username, group IDs, and tenant IDs into the engine's thread execution. 15 | *

16 | * 17 | * @author Stephen Russett Github:StephenOTT 18 | */ 19 | @CompileStatic 20 | public class AuthenticationFilterJwt implements AuthenticationProviderJwt { 21 | 22 | private static final Logger LOG = LoggerFactory.getLogger(AuthenticationFilterJwt.class) 23 | protected static final String JWT_AUTH_HEADER_PREFIX = "Bearer "; 24 | 25 | @Override 26 | public AuthenticationResultJwt extractAuthenticatedUser(HttpServletRequest request, 27 | ProcessEngine engine, 28 | Class jwtValidatorClass, 29 | String jwtSecretPath) { 30 | 31 | String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); 32 | 33 | if (authorizationHeader != null && authorizationHeader.startsWith(JWT_AUTH_HEADER_PREFIX)) { 34 | String encodedCredentials = authorizationHeader.substring(JWT_AUTH_HEADER_PREFIX.length()) 35 | 36 | // Load the specific class defined in String jwtValidator variable. 37 | ValidatorResultJwt validatorResult 38 | try{ 39 | AbstractValidatorJwt validator = (AbstractValidatorJwt)jwtValidatorClass.newInstance() 40 | validatorResult = validator.validateJwt(encodedCredentials, jwtSecretPath) 41 | } catch(all){ 42 | // @TODO Add better Exception handling for JWT Validator class loading 43 | LOG.error("Could not load Jwt Validator Class: ${all.getLocalizedMessage()}") 44 | return AuthenticationResultJwt.unsuccessful() 45 | } 46 | 47 | if (validatorResult.getResult()){ 48 | return AuthenticationResultJwt.successful(validatorResult.getAuthenticatedUsername(), 49 | validatorResult.getGroupIds(), 50 | validatorResult.getTenantIds()) 51 | } else { 52 | return AuthenticationResultJwt.unsuccessful() 53 | } 54 | 55 | } else { 56 | LOG.error('JWT: missing JWT header') 57 | return AuthenticationResultJwt.unsuccessful(); 58 | } 59 | } 60 | 61 | @Override 62 | public void augmentResponseByAuthenticationChallenge( 63 | HttpServletResponse response, ProcessEngine engine) { 64 | response.setHeader(HttpHeaders.WWW_AUTHENTICATE, JWT_AUTH_HEADER_PREFIX + "realm=\"" + engine.getName() + "\""); 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/groovy/io/digitalstate/camunda/authentication/jwt/AuthenticationProviderJwt.groovy: -------------------------------------------------------------------------------- 1 | 2 | package io.digitalstate.camunda.authentication.jwt 3 | 4 | import groovy.transform.CompileStatic 5 | import org.camunda.bpm.engine.ProcessEngine; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | /** 11 | * A provider to handle the authentication of {@link HttpServletRequest}s. 12 | * May implement a specific authentication scheme. 13 | * Has been modified from the original source, to support JWT authentication 14 | * 15 | * @author Thorben Lindhauer 16 | * @author Stpehen Russett 17 | */ 18 | @CompileStatic 19 | public interface AuthenticationProviderJwt { 20 | 21 | /** 22 | * Checks the request for authentication. May not return null, but always an {@link AuthenticationResultJwt} that indicates, whether 23 | * authentication was successful, and, if true, always provides the authenticated user, and optionally group IDs and tenant IDs. 24 | * 25 | * @param request the request to authenticate 26 | * @param engine the process engine the request addresses. 27 | * @param jwtValidator the fully qualified class name that extends AbstractValidatorJwt, that will be used to validate the JWT. 28 | * @param jwtSecretPath the file path of the location of the secret used to decode/validate the JWT. May be null if secret is pulled from another location. 29 | */ 30 | AuthenticationResultJwt extractAuthenticatedUser(HttpServletRequest request, ProcessEngine engine, Class jwtValidator, String jwtSecretPath); 31 | 32 | /** 33 | *

34 | * Callback to add an authentication challenge to the response to the client. Called in case of unsuccessful authentication. 35 | *

36 | * 37 | *

38 | * For example, a Http Basic auth implementation may set the WWW-Authenticate header to Basic realm="engine name". 39 | *

40 | * 41 | * @param request the response to augment 42 | * @param engine the process engine the request addressed. May be considered as an authentication realm to create a specific authentication 43 | * challenge 44 | */ 45 | void augmentResponseByAuthenticationChallenge(HttpServletResponse response, ProcessEngine engine); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/groovy/io/digitalstate/camunda/authentication/jwt/AuthenticationResultJwt.groovy: -------------------------------------------------------------------------------- 1 | package io.digitalstate.camunda.authentication.jwt 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | public class AuthenticationResultJwt { 7 | 8 | protected boolean isAuthenticated; 9 | 10 | protected String authenticatedUser; 11 | protected List groups; 12 | protected List tenants; 13 | 14 | public AuthenticationResultJwt(String authenticatedUser, boolean isAuthenticated, List groups, List tenants) { 15 | this.authenticatedUser = authenticatedUser; 16 | this.isAuthenticated = isAuthenticated; 17 | this.groups = groups 18 | this.tenants = tenants 19 | } 20 | 21 | public String getAuthenticatedUser() { 22 | return authenticatedUser; 23 | } 24 | 25 | public void setAuthenticatedUser(String authenticatedUser) { 26 | this.authenticatedUser = authenticatedUser; 27 | } 28 | 29 | public boolean isAuthenticated() { 30 | return isAuthenticated; 31 | } 32 | 33 | public void setAuthenticated(boolean isAuthenticated) { 34 | this.isAuthenticated = isAuthenticated; 35 | } 36 | 37 | public List getGroups() { 38 | return groups; 39 | } 40 | 41 | public void setGroups(List groups) { 42 | this.groups = groups; 43 | } 44 | 45 | public List getTenants() { 46 | return tenants; 47 | } 48 | 49 | public void setTenants(List tenants) { 50 | this.tenants = tenants; 51 | } 52 | 53 | public static AuthenticationResultJwt successful(String userId, List groups = null, List tenants = null) { 54 | return new AuthenticationResultJwt(userId, true, groups, tenants); 55 | } 56 | 57 | public static AuthenticationResultJwt unsuccessful() { 58 | return new AuthenticationResultJwt(null, false, null, null); 59 | } 60 | 61 | public static AuthenticationResultJwt unsuccessful(String userId) { 62 | return new AuthenticationResultJwt(userId, false, null, null); 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/groovy/io/digitalstate/camunda/authentication/jwt/ProcessEngineAuthenticationFilterJwt.groovy: -------------------------------------------------------------------------------- 1 | package io.digitalstate.camunda.authentication.jwt 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | import javax.servlet.Filter; 6 | import javax.servlet.FilterChain; 7 | import javax.servlet.FilterConfig; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.ServletRequest; 10 | import javax.servlet.ServletResponse; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import javax.ws.rs.core.MediaType; 14 | import javax.ws.rs.core.Response.Status; 15 | 16 | import org.camunda.bpm.BpmPlatform; 17 | import org.camunda.bpm.engine.ProcessEngine; 18 | import org.camunda.bpm.engine.ProcessEngines; 19 | 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | 22 | /** 23 | *

24 | * Servlet filter to plug in authentication. 25 | *

26 | * 27 | *

Valid init-params:

28 | * 29 | * 30 | * 31 | * 32 | * 33 | * 34 | * 35 | * 36 | * 37 | * 39 | * 40 | *
ParameterRequiredExpected value
{@value #AUTHENTICATION_PROVIDER_PARAM}yesAn implementation of {@link AuthenticationProvider}
{@value #SERVLET_PATH_PREFIX}noThe expected servlet path. Should only be set, if the underlying JAX-RS application is not deployed as a servlet (e.g. Resteasy allows deployments 38 | * as a servlet filter). Value has to match what would be the {@link HttpServletRequest#getServletPath()} if it was deployed as a servlet.
41 | * 42 | * Has been modified from original source to remove group and tenant getters using Camunda DB, and expects the group and tenant IDs to be provided by the ValidatorJWT implementation. 43 | * 44 | * @author Thorben Lindhauer 45 | * @author Stephen Russett 46 | */ 47 | 48 | @CompileStatic 49 | public class ProcessEngineAuthenticationFilterJwt implements Filter { 50 | 51 | // init params 52 | public static final String AUTHENTICATION_PROVIDER_PARAM = "authentication-provider"; 53 | public static final String JWT_SECRET_PATH_PARAM = "jwt-secret-path"; 54 | public static final String JWT_VALIDATOR_PARAM = "jwt-validator"; 55 | private static String jwtSecretPath 56 | private static String jwtValidator 57 | private static Class jwtValidatorClass 58 | 59 | 60 | protected AuthenticationProviderJwt authenticationProvider; 61 | 62 | @Override 63 | public void init(FilterConfig filterConfig) throws ServletException { 64 | String authenticationProviderClassName = filterConfig.getInitParameter(AUTHENTICATION_PROVIDER_PARAM); 65 | 66 | if (!jwtSecretPath){ 67 | jwtSecretPath = filterConfig.getInitParameter(JWT_SECRET_PATH_PARAM) 68 | } 69 | 70 | if (!jwtValidator){ 71 | jwtValidator = filterConfig.getInitParameter(JWT_VALIDATOR_PARAM) 72 | } 73 | 74 | if (authenticationProviderClassName == null) { 75 | throw new ServletException("Cannot instantiate authentication filter: no authentication provider set. init-param " + AUTHENTICATION_PROVIDER_PARAM + " missing"); 76 | } 77 | 78 | try { 79 | Class authenticationProviderClass = Class.forName(authenticationProviderClassName); 80 | authenticationProvider = (AuthenticationProviderJwt) authenticationProviderClass.newInstance(); 81 | 82 | } catch (ClassNotFoundException e) { 83 | throw new ServletException("Cannot instantiate authentication filter: authentication provider not found", e); 84 | } catch (InstantiationException e) { 85 | throw new ServletException("Cannot instantiate authentication filter: cannot instantiate authentication provider", e); 86 | } catch (IllegalAccessException e) { 87 | throw new ServletException("Cannot instantiate authentication filter: constructor not accessible", e); 88 | } catch (ClassCastException e) { 89 | throw new ServletException("Cannot instantiate authentication filter: authentication provider does not implement interface " + 90 | AuthenticationProviderJwt.class.getName(), e); 91 | } 92 | 93 | try{ 94 | jwtValidatorClass = getClass().getClassLoader().loadClass(jwtValidator) 95 | } catch(all){ 96 | // @TODO Add better Exception handling for JWT Validator class loading 97 | throw new ServletException("Could not load Jwt Validator Class: ${all.getLocalizedMessage()}") 98 | } 99 | 100 | } 101 | 102 | @Override 103 | public void doFilter(ServletRequest request, ServletResponse response, 104 | FilterChain chain) throws IOException, ServletException { 105 | 106 | HttpServletRequest req = (HttpServletRequest) request; 107 | HttpServletResponse resp = (HttpServletResponse) response; 108 | 109 | 110 | ProcessEngine engine = BpmPlatform.getDefaultProcessEngine(); 111 | 112 | if (engine == null) { 113 | engine = ProcessEngines.getDefaultProcessEngine(false); 114 | } 115 | 116 | if (engine == null) { 117 | resp.setStatus(Status.NOT_FOUND.getStatusCode()); 118 | String errMessage = "Default Process engine not available"; 119 | ObjectMapper objectMapper = new ObjectMapper(); 120 | 121 | resp.setContentType(MediaType.APPLICATION_JSON); 122 | objectMapper.writer().writeValue(resp.getWriter(), errMessage); 123 | resp.getWriter().flush(); 124 | 125 | return; 126 | } 127 | 128 | AuthenticationResultJwt authenticationResult = authenticationProvider.extractAuthenticatedUser(req, engine, jwtValidatorClass, jwtSecretPath); 129 | 130 | if (authenticationResult.isAuthenticated()) { 131 | try { 132 | String authenticatedUser = authenticationResult.getAuthenticatedUser() 133 | 134 | // @TODO Review if null or empty array should be sent into Groups and Tenants when JWT does not have these claims 135 | List groupIds = authenticationResult.getGroups() ?: [] 136 | List tenantIds = authenticationResult.getTenants() ?: [] 137 | 138 | setAuthenticatedUser(engine, authenticatedUser, groupIds, tenantIds ); 139 | 140 | chain.doFilter(request, response); 141 | 142 | } finally { 143 | clearAuthentication(engine); 144 | } 145 | } else { 146 | resp.setStatus(Status.UNAUTHORIZED.getStatusCode()); 147 | authenticationProvider.augmentResponseByAuthenticationChallenge(resp, engine); 148 | } 149 | } 150 | 151 | @Override 152 | public void destroy() { 153 | } 154 | 155 | protected void setAuthenticatedUser(ProcessEngine engine, String userId, List groupIds, List tenantIds) { 156 | engine.getIdentityService().setAuthentication(userId, groupIds, tenantIds); 157 | } 158 | 159 | protected void clearAuthentication(ProcessEngine engine) { 160 | engine.getIdentityService().clearAuthentication(); 161 | } 162 | 163 | } -------------------------------------------------------------------------------- /src/main/groovy/io/digitalstate/camunda/authentication/jwt/ValidatorResultJwt.groovy: -------------------------------------------------------------------------------- 1 | package io.digitalstate.camunda.authentication.jwt 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | @CompileStatic 6 | public class ValidatorResultJwt { 7 | Boolean result 8 | String authenticatedUsername 9 | List groupIds 10 | List tenantIds 11 | 12 | public ValidatorResultJwt(Boolean result, String authenticatedUsername, List groupIds, List tenantIds){ 13 | this.result = result 14 | this.authenticatedUsername = authenticatedUsername 15 | this.groupIds = groupIds 16 | this.tenantIds = tenantIds 17 | } 18 | 19 | public static ValidatorResultJwt setValidatorResult( Boolean result, String authenticatedUsername, List groupIds, List tenantIds){ 20 | return new ValidatorResultJwt(result, authenticatedUsername, groupIds, tenantIds) 21 | } 22 | 23 | public Boolean getResult(){ 24 | return this.result 25 | } 26 | public String getAuthenticatedUsername(){ 27 | return this.authenticatedUsername 28 | } 29 | public List getGroupIds(){ 30 | return this.groupIds 31 | } 32 | public List getTenantIds(){ 33 | return this.tenantIds 34 | } 35 | } 36 | --------------------------------------------------------------------------------