├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── org │ └── secnod │ └── shiro │ ├── jaxrs │ ├── Auth.java │ └── ShiroExceptionMapper.java │ └── jersey │ ├── AuthorizationFilter.java │ ├── AuthorizationFilterFeature.java │ ├── SubjectFactory.java │ └── TypeFactory.java └── test ├── java └── org │ └── secnod │ ├── example │ └── webapp │ │ ├── ExampleApplication.java │ │ ├── HelloWorldResource.java │ │ ├── User.java │ │ └── UserFactory.java │ └── shiro │ └── test │ └── integration │ ├── AnnotationAuthTest.java │ ├── IntegrationTestSuite.java │ └── webapp │ ├── FieldInjectionResource.java │ ├── GuestOnlyResource.java │ ├── InjectionResource.java │ ├── IntegrationTestApplication.java │ ├── JettyServer.java │ ├── OptionalBasicHttpAuthenticationFilter.java │ ├── PermissionProtectedResource.java │ ├── PublicResource.java │ ├── RequireAuthenticatedUserResource.java │ ├── RequireUserResource.java │ ├── RoleProtectedResource.java │ ├── SessionResource.java │ ├── SubjectAuthResource.java │ └── UserAuthResource.java └── resources ├── logging.properties ├── org └── secnod │ ├── example │ └── webapp │ │ └── WEB-INF │ │ └── web.xml │ └── shiro │ └── test │ └── integration │ └── webapp │ ├── 404.html │ ├── WEB-INF │ ├── shiro.ini │ └── web.xml │ └── index.html └── shiro.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | target 3 | .settings 4 | .classpath 5 | infinitest.filters 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Apache Shiro support for the Jersey JAX-RS implementation. 2 | 3 | # News 4 | 5 | [Shiro 1.4](http://shiro.apache.org/news.html#1.4.0-RC2-released) has been 6 | released and includes a new official JAX-RS module `shiro-jaxrs` based on `shiro-jersey`. 7 | 8 | The official `shiro-jaxrs` module offers feature parity with the generic JAX-RS 9 | functionality of `shiro-jersey`. The main difference is that `shiro-jaxrs` does 10 | not support the Jersey specific injections of `shiro-jersey`. 11 | 12 | See: 13 | * [Apache Shiro JAX-RS Support](https://shiro.apache.org/jaxrs.html) 14 | * [the source code](https://github.com/apache/shiro/tree/shiro-root-1.4.0/support/jaxrs/src/main/java/org/apache/shiro/web/jaxrs) 15 | * [SHIRO-392](https://issues.apache.org/jira/browse/SHIRO-392) 16 | * [Example usage of the Shiro JAX-RS module](https://stormpath.com/blog/protecting-jax-rs-resources-rbac-apache-shIro) ([Wayback Machine](http://web.archive.org/web/20230129054611/https://stormpath.com/blog/protecting-jax-rs-resources-rbac-apache-shIro)) 17 | 18 | # Adding the shiro-jersey dependency 19 | 20 | Add the following dependencies to `pom.xml` in an existing project already using Jersey: 21 | 22 | ```xml 23 | 24 | org.secnod.shiro 25 | shiro-jersey 26 | 0.2.0 27 | 28 | ``` 29 | 30 | Version compatibility: 31 | 32 | |Jersey |Shiro Jersey| Compatibility | 33 | |--------|------------|------| 34 | |3.x |0.4.0-SNAPSHOT| Jakarta EE | 35 | |2.26- |0.3.0 | Java EE | 36 | |2.0-2.25|0.2.0 | Java EE | 37 | |1.x |0.1.1 | Java EE | 38 | 39 | If you are upgrading from: 40 | 41 | * Jersey 2.0-2.25, see the [upgrade instructions](#mig-0.2.x). 42 | * Jersey 1.x, see the [upgrade instructions](#mig-0.1.x). 43 | 44 | # Configuring Shiro in a Jersey web application 45 | 46 | An example web application is provided complete with [source code](src/test/java/org/secnod/example/webapp) 47 | and [web content](src/test/resources/org/secnod/example/webapp). 48 | 49 | The rest of this section describes how Shiro has been added to the example application. 50 | 51 | Add the Shiro servlet filter in `web.xml`: 52 | 53 | ```xml 54 | 55 | 56 | shiroConfigLocations 57 | classpath:shiro.ini 58 | 59 | 60 | 61 | org.apache.shiro.web.env.EnvironmentLoaderListener 62 | 63 | 64 | 65 | ShiroFilter 66 | org.apache.shiro.web.servlet.ShiroFilter 67 | 68 | 69 | 70 | ShiroFilter 71 | /* 72 | REQUEST 73 | FORWARD 74 | INCLUDE 75 | ERROR 76 | 77 | ``` 78 | 79 | Then register the following components in the JAX-RS application: 80 | 81 | ```java 82 | public class ApiApplication extends ResourceConfig { 83 | public ApiApplication() { 84 | register(org.apache.shiro.web.jaxrs.ShiroFeature.class); 85 | register(new SubjectFactory()); 86 | } 87 | } 88 | ``` 89 | 90 | ## Configuring Shiro 91 | Finally configure `shiro.ini` in the default package on the classpath: 92 | 93 | ```ini 94 | [main] 95 | 96 | [users] 97 | exampleuser = examplepassword, examplerole 98 | 99 | [roles] 100 | examplerole = something:readpermission 101 | 102 | [urls] 103 | /** = noSessionCreation, authcBasic 104 | ``` 105 | 106 | Real applications should of course not store users and passwords in the INI-file. See the 107 | [Shiro configuration documentation](http://shiro.apache.org/configuration.html). 108 | 109 | # Using Shiro from JAX-RS 110 | 111 | This section describes the different alternatives for how Shiro can be used from JAX-RS. 112 | 113 | ## Declarative authorization with annotations 114 | 115 | JAX-RS resource classes and methods can be annotated with the 116 | [standard Shiro annotations](http://shiro.apache.org/static/1.4.2/apidocs/org/apache/shiro/authz/annotation/package-summary.html). 117 | 118 | The authorization requirements can for example be declared with `@RequiresPermissions` on JAX-RS resource classes / 119 | methods: 120 | 121 | ```java 122 | @Path("/auth") 123 | @Produces(MediaType.TEXT_PLAIN) 124 | @RequiresPermissions("protected:read") 125 | public class AuthResource { 126 | 127 | @GET 128 | public String get() { 129 | return "OK"; 130 | } 131 | 132 | @PUT 133 | @RequiresPermissions("protected:write") 134 | public String set(String value) { 135 | return value; 136 | } 137 | } 138 | ``` 139 | 140 | The example above can be summarized as: 141 | 142 | * HTTP GET access requires the user to have the permission `protected:read` 143 | * HTTP PUT access requires the user to have both permissions `protected:read` and `protected:write` 144 | 145 | ## Programmatic authorization 146 | 147 | Programmatic authorization is done by injecting the Shiro 148 | [Subject](http://shiro.apache.org/static/1.4.2/apidocs/org/apache/shiro/subject/Subject.html) as a method parameter: 149 | 150 | ```java 151 | @Path("/auth") 152 | @Produces(MediaType.TEXT_PLAIN) 153 | public class AuthResource { 154 | 155 | @GET 156 | public String get(@Auth Subject subject) { 157 | subject.checkPermission("protected:read"); 158 | return "OK"; 159 | } 160 | } 161 | ``` 162 | 163 | Injecting the Subject is just a convenience over calling 164 | [SecurityUtils.getSubject()](http://shiro.apache.org/static/1.4.2/apidocs/org/apache/shiro/SecurityUtils.html#getSubject()). 165 | 166 | Declarative and programmatic authorization are often combined when some permissions are static and some are dynamic: 167 | 168 | ```java 169 | @Path("/auth") 170 | @Produces(MediaType.TEXT_PLAIN) 171 | public class AuthResource { 172 | 173 | @GET 174 | @RequiresPermissions("static-permission") 175 | public String get(@Auth Subject subject) { 176 | subject.checkPermission(dynamicPermission()); 177 | return "OK"; 178 | } 179 | } 180 | ``` 181 | 182 | ## Optionally using an application specific user class 183 | 184 | Instead of using the Shiro `Subject` class directly one can use an application specific user class for programmatic 185 | authorization: 186 | 187 | ```java 188 | @Path("/auth") 189 | @Produces(MediaType.TEXT_PLAIN) 190 | public class AuthResource { 191 | 192 | @GET 193 | public String get(@Auth User user) { 194 | user.checkBusinessRulePermission(); 195 | return "OK"; 196 | } 197 | } 198 | ``` 199 | 200 | A custom `User` class is a convenient way of implementing application 201 | specific authorization based on business rules on the user's data. 202 | 203 | More authorization as rules means less authorization as permissions and hence fewer permissions to maintain. 204 | 205 | See: 206 | * The example [User](src/test/java/org/secnod/example/webapp/User.java) class. 207 | * The example [UserFactory](src/test/java/org/secnod/example/webapp/UserFactory.java) 208 | which must be registered as a JAX-RS component. 209 | * The class [TypeFactory](src/main/java/org/secnod/shiro/jersey/TypeFactory.java) 210 | can be extended for injection of custom classes with the `@Auth` annotation. 211 | 212 | ## Migrating from 0.2.x 213 | 214 | `AuthInjectionBinder` has been deleted. Remove its registration in 215 | `ResourceConfig.register()`. 216 | 217 | ## Migrating from 0.1.x 218 | 219 | These instructions assume that the JAX-RS application is a subclass of 220 | `org.glassfish.jersey.server.ResourceConfig`. 221 | 222 | Note that JAX-RS component registration is done by `ResourceConfig.register()` 223 | instead of `javax.ws.rs.core.Application.getSingletons()`. 224 | 225 | * `AuthorizationFilterFeature` replaces `ShiroResourceFilterFactory` 226 | 227 | Remove the configuration of `ShiroResourceFilterFactory` from `web.xml` and 228 | register `AuthorizationFilterFeature` as a JAX-RS component. 229 | 230 | * `SubjectFactory` replaces `SubjectInjectableProvider` 231 | * `TypeFactory` replaces `AuthInjectableProvider` 232 | 233 | # Development 234 | ## Running the integration tests 235 | 236 | The integration tests for this project can be run as follows: 237 | 238 | mvn -Pintegration-tests test -Dshiro.jersey=false 239 | 240 | The default is to run the tests using the Apache Shiro [JAX-RS support](https://repo1.maven.org/maven2/org/apache/shiro/shiro-jaxrs/1.4.2/). 241 | Alternatively, the old `shiro-jersey` features can be enabled instead by setting `shiro.jersey` to `true`. 242 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.secnod.shiro 6 | shiro-jersey 7 | 0.4.0-SNAPSHOT 8 | 9 | ${project.groupId}:${project.artifactId} 10 | Apache Shiro support for the Jersey JAX-RS implementation. 11 | https://github.com/silb/shiro-jersey 12 | 13 | 14 | 15 | Apache License 2.0 16 | http://www.apache.org/licenses/LICENSE-2.0.html 17 | repo 18 | 19 | 20 | 21 | 22 | 23 | Stig Inge Lea Bjørnsen 24 | stiginge@pvv.org 25 | http://blog.silb.no 26 | 27 | 28 | 29 | 30 | scm:git:https://github.com/silb/shiro-jersey.git 31 | scm:git:git@github.com:silb/shiro-jersey.git 32 | HEAD 33 | https://github.com/silb/shiro-jersey 34 | 35 | 36 | 37 | https://github.com/silb/shiro-jersey/issues 38 | 39 | 40 | 41 | 42 | ossrh 43 | https://oss.sonatype.org/content/repositories/snapshots 44 | 45 | 46 | ossrh 47 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 48 | 49 | 50 | 51 | 52 | 53 | org.apache.shiro 54 | shiro-core 55 | ${shiro.version} 56 | 57 | 58 | org.apache.shiro 59 | shiro-jaxrs 60 | ${shiro.version} 61 | 62 | 63 | org.apache.shiro 64 | shiro-web 65 | ${shiro.version} 66 | runtime 67 | 68 | 69 | javax.ws.rs 70 | javax.ws.rs-api 71 | 2.1 72 | 73 | 74 | org.glassfish.jersey.core 75 | jersey-server 76 | ${jersey.version} 77 | 78 | 79 | org.glassfish.jersey.containers 80 | jersey-container-servlet 81 | ${jersey.version} 82 | 83 | 84 | org.glassfish.jersey.inject 85 | jersey-hk2 86 | ${jersey.version} 87 | 88 | 89 | junit 90 | junit 91 | 4.12 92 | test 93 | 94 | 95 | org.slf4j 96 | slf4j-api 97 | ${slf4j.version} 98 | test 99 | 100 | 101 | org.slf4j 102 | slf4j-jdk14 103 | ${slf4j.version} 104 | test 105 | 106 | 107 | org.glassfish.jersey.core 108 | jersey-client 109 | ${jersey.version} 110 | test 111 | 112 | 113 | org.glassfish.jersey.connectors 114 | jersey-apache-connector 115 | ${jersey.version} 116 | test 117 | 118 | 119 | org.eclipse.jetty 120 | jetty-webapp 121 | ${jetty.version} 122 | test 123 | 124 | 125 | com.fasterxml.jackson.jaxrs 126 | jackson-jaxrs-json-provider 127 | 2.6.3 128 | test 129 | 130 | 131 | 132 | 133 | UTF-8 134 | UTF-8 135 | 1.7.12 136 | 2.26 137 | 9.2.13.v20150730 138 | 1.4.2 139 | 1.8 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-javadoc-plugin 149 | 2.8 150 | 151 | 152 | 153 | 154 | 155 | 156 | org.apache.maven.plugins 157 | maven-compiler-plugin 158 | 2.3.2 159 | 160 | ${jdk.version} 161 | ${jdk.version} 162 | true 163 | -Xlint 164 | true 165 | 166 | 167 | 168 | 169 | org.apache.maven.plugins 170 | maven-enforcer-plugin 171 | 1.3.1 172 | 173 | 174 | enforce-versions 175 | 176 | enforce 177 | 178 | 179 | 180 | 181 | [${jdk.version},1.9] 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | org.codehaus.mojo 191 | exec-maven-plugin 192 | 1.2.1 193 | 194 | 195 | 196 | exec 197 | 198 | 199 | 200 | 201 | java 202 | test 203 | 204 | -Djava.util.logging.config.file=src/test/resources/logging.properties 205 | -classpath 206 | 207 | org.secnod.shiro.test.integration.webapp.JettyServer 208 | 209 | 210 | 211 | 212 | 213 | org.apache.maven.plugins 214 | maven-surefire-plugin 215 | 2.16 216 | 217 | 218 | **/test/integration/**/*Test.java 219 | 220 | 221 | 222 | 223 | 224 | org.apache.maven.plugins 225 | maven-eclipse-plugin 226 | 2.8 227 | 228 | true 229 | true 230 | 231 | org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-${jdk.version} 232 | 233 | 234 | 235 | 236 | 237 | org.apache.maven.plugins 238 | maven-source-plugin 239 | 2.1.2 240 | 241 | 242 | attach-sources 243 | 244 | jar-no-fork 245 | test-jar-no-fork 246 | 247 | 248 | 249 | 250 | 251 | true 252 | 253 | 254 | 255 | 256 | org.apache.maven.plugins 257 | maven-jar-plugin 258 | 2.3.1 259 | 260 | 261 | 262 | test-jar 263 | 264 | 265 | 266 | 267 | 268 | 269 | org.apache.maven.plugins 270 | maven-release-plugin 271 | 2.4.2 272 | 273 | 274 | org.apache.maven.scm 275 | maven-scm-provider-gitexe 276 | 1.8.1 277 | 278 | 279 | 280 | v@{project.version} 281 | false 282 | release 283 | 284 | 285 | 286 | 287 | org.apache.maven.plugins 288 | maven-javadoc-plugin 289 | 290 | 291 | https://eclipse-ee4j.github.io/jersey.github.io/apidocs/${jersey.version}/jersey/ 292 | http://shiro.apache.org/static/${shiro.version}/apidocs/ 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | integration-tests 304 | 305 | 306 | 307 | org.apache.maven.plugins 308 | maven-surefire-plugin 309 | 2.16 310 | 311 | 312 | **/test/integration/IntegrationTestSuite.java 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | release 322 | 323 | 324 | 325 | 326 | org.apache.maven.plugins 327 | maven-javadoc-plugin 328 | 329 | 330 | attach-javadocs 331 | 332 | jar 333 | 334 | 335 | 336 | 337 | 338 | 339 | org.apache.maven.plugins 340 | maven-gpg-plugin 341 | 1.5 342 | 343 | 344 | sign-artifacts 345 | verify 346 | 347 | sign 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | infinitest 360 | 361 | 362 | 363 | org.apache.maven.plugins 364 | maven-eclipse-plugin 365 | 2.8 366 | 367 | 368 | 369 | infinitest.filters 370 | 371 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | -------------------------------------------------------------------------------- /src/main/java/org/secnod/shiro/jaxrs/Auth.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.jaxrs; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Auth { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/secnod/shiro/jaxrs/ShiroExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.jaxrs; 2 | 3 | import javax.ws.rs.core.Response; 4 | import javax.ws.rs.core.Response.Status; 5 | import javax.ws.rs.ext.ExceptionMapper; 6 | 7 | import org.apache.shiro.authz.AuthorizationException; 8 | import org.apache.shiro.authz.UnauthorizedException; 9 | 10 | /** 11 | * @deprecated replaced by the native Shiro exception mapper {@link org.apache.shiro.web.jaxrs.ExceptionMapper} 12 | * 13 | * @see org.apache.shiro.web.jaxrs.ShiroFeature 14 | */ 15 | @Deprecated 16 | public class ShiroExceptionMapper implements ExceptionMapper { 17 | 18 | @Override 19 | public Response toResponse(AuthorizationException exception) { 20 | 21 | Status status; 22 | 23 | if (exception instanceof UnauthorizedException) { 24 | status = Status.FORBIDDEN; 25 | } else { 26 | status = Status.UNAUTHORIZED; 27 | } 28 | 29 | return Response.status(status).build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/secnod/shiro/jersey/AuthorizationFilter.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.jersey; 2 | 3 | import java.io.IOException; 4 | import java.lang.annotation.Annotation; 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | import javax.ws.rs.container.ContainerRequestContext; 11 | import javax.ws.rs.container.ContainerRequestFilter; 12 | 13 | import org.apache.shiro.authz.annotation.RequiresAuthentication; 14 | import org.apache.shiro.authz.annotation.RequiresGuest; 15 | import org.apache.shiro.authz.annotation.RequiresPermissions; 16 | import org.apache.shiro.authz.annotation.RequiresRoles; 17 | import org.apache.shiro.authz.annotation.RequiresUser; 18 | import org.apache.shiro.authz.aop.AuthenticatedAnnotationHandler; 19 | import org.apache.shiro.authz.aop.AuthorizingAnnotationHandler; 20 | import org.apache.shiro.authz.aop.GuestAnnotationHandler; 21 | import org.apache.shiro.authz.aop.PermissionAnnotationHandler; 22 | import org.apache.shiro.authz.aop.RoleAnnotationHandler; 23 | import org.apache.shiro.authz.aop.UserAnnotationHandler; 24 | 25 | /** 26 | * A filter that grants or denies access to a JAX-RS resource based on the Shiro annotations on it. 27 | * 28 | * @see org.apache.shiro.authz.annotation 29 | * 30 | * @deprecated replaced by the native Shiro filter {@link org.apache.shiro.web.jaxrs.AnnotationAuthorizationFilter} 31 | */ 32 | @Deprecated 33 | public class AuthorizationFilter implements ContainerRequestFilter { 34 | 35 | private final Map authzChecks; 36 | 37 | public AuthorizationFilter(Collection authzSpecs) { 38 | Map authChecks = new HashMap<>(authzSpecs.size()); 39 | for (Annotation authSpec : authzSpecs) { 40 | authChecks.put(createHandler(authSpec), authSpec); 41 | } 42 | this.authzChecks = Collections.unmodifiableMap(authChecks); 43 | } 44 | 45 | private static AuthorizingAnnotationHandler createHandler(Annotation annotation) { 46 | Class t = annotation.annotationType(); 47 | if (RequiresPermissions.class.equals(t)) return new PermissionAnnotationHandler(); 48 | else if (RequiresRoles.class.equals(t)) return new RoleAnnotationHandler(); 49 | else if (RequiresUser.class.equals(t)) return new UserAnnotationHandler(); 50 | else if (RequiresGuest.class.equals(t)) return new GuestAnnotationHandler(); 51 | else if (RequiresAuthentication.class.equals(t)) return new AuthenticatedAnnotationHandler(); 52 | else throw new IllegalArgumentException("Cannot create a handler for the unknown for annotation " + t); 53 | } 54 | 55 | @Override 56 | public void filter(ContainerRequestContext requestContext) throws IOException { 57 | for (Map.Entry authzCheck : authzChecks.entrySet()) { 58 | AuthorizingAnnotationHandler handler = authzCheck.getKey(); 59 | Annotation authzSpec = authzCheck.getValue(); 60 | handler.assertAuthorized(authzSpec); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/secnod/shiro/jersey/AuthorizationFilterFeature.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.jersey; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | import javax.ws.rs.Priorities; 10 | import javax.ws.rs.container.DynamicFeature; 11 | import javax.ws.rs.container.ResourceInfo; 12 | import javax.ws.rs.core.FeatureContext; 13 | 14 | import org.apache.shiro.authz.annotation.RequiresAuthentication; 15 | import org.apache.shiro.authz.annotation.RequiresGuest; 16 | import org.apache.shiro.authz.annotation.RequiresPermissions; 17 | import org.apache.shiro.authz.annotation.RequiresRoles; 18 | import org.apache.shiro.authz.annotation.RequiresUser; 19 | 20 | /** 21 | * Wraps {@link AuthorizationFilter filters} around JAX-RS resources that are 22 | * annotated with Shiro annotations. 23 | * 24 | * @see org.apache.shiro.web.jaxrs.ShiroFeature 25 | * 26 | * @deprecated replaced by the native Shiro filter feature 27 | * {@link org.apache.shiro.web.jaxrs.ShiroAnnotationFilterFeature} 28 | */ 29 | @Deprecated 30 | public class AuthorizationFilterFeature implements DynamicFeature { 31 | 32 | private static List> shiroAnnotations = Collections.unmodifiableList(Arrays.asList( 33 | RequiresPermissions.class, 34 | RequiresRoles.class, 35 | RequiresAuthentication.class, 36 | RequiresUser.class, 37 | RequiresGuest.class)); 38 | 39 | @Override 40 | public void configure(ResourceInfo resourceInfo, FeatureContext context) { 41 | 42 | List authzSpecs = new ArrayList<>(); 43 | 44 | for (Class annotationClass : shiroAnnotations) { 45 | // XXX What is the performance of getAnnotation vs getAnnotations? 46 | Annotation classAuthzSpec = resourceInfo.getResourceClass().getAnnotation(annotationClass); 47 | Annotation methodAuthzSpec = resourceInfo.getResourceMethod().getAnnotation(annotationClass); 48 | 49 | if (classAuthzSpec != null) authzSpecs.add(classAuthzSpec); 50 | if (methodAuthzSpec != null) authzSpecs.add(methodAuthzSpec); 51 | } 52 | 53 | if (!authzSpecs.isEmpty()) { 54 | context.register(new AuthorizationFilter(authzSpecs), Priorities.AUTHORIZATION); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/secnod/shiro/jersey/SubjectFactory.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.jersey; 2 | 3 | import org.apache.shiro.SecurityUtils; 4 | import org.apache.shiro.subject.Subject; 5 | import org.glassfish.hk2.api.PerLookup; 6 | 7 | /** 8 | * Creates {@link Subject subjects} to be used as injected values. 9 | */ 10 | public class SubjectFactory extends TypeFactory { 11 | 12 | public SubjectFactory() { 13 | super(Subject.class); 14 | } 15 | 16 | @PerLookup 17 | @Override 18 | public Subject provide() { 19 | return SecurityUtils.getSubject(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/secnod/shiro/jersey/TypeFactory.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.jersey; 2 | 3 | import java.util.function.Function; 4 | 5 | import org.glassfish.jersey.server.ContainerRequest; 6 | import org.glassfish.jersey.server.model.Parameter; 7 | import org.glassfish.jersey.server.spi.internal.ValueParamProvider; 8 | import org.secnod.shiro.jaxrs.Auth; 9 | 10 | /** 11 | * Base class for factories that can instantiate object of a given type. 12 | * 13 | * @param the type of the objects that the factory creates 14 | */ 15 | public abstract class TypeFactory implements ValueParamProvider, Function { 16 | public final Class type; 17 | 18 | public TypeFactory(Class type) { 19 | this.type = type; 20 | } 21 | 22 | /* 23 | * This class used to implement {@code org.glassfish.hk2.api.Factory} providing this method. 24 | * This method provides backwards compatibility for existing subclasses. 25 | */ 26 | public abstract T provide(); 27 | 28 | // org.glassfish.jersey.server.spi.internal.ValueParamProvider 29 | 30 | @Override 31 | public Function getValueProvider(Parameter parameter) { 32 | if (type.equals(parameter.getRawType()) && parameter.isAnnotationPresent(Auth.class)) { 33 | return this; 34 | } 35 | return null; 36 | } 37 | 38 | @Override 39 | public PriorityType getPriority() { 40 | return Priority.NORMAL; 41 | } 42 | 43 | // java.util.Function 44 | 45 | @Override 46 | public T apply(ContainerRequest request) { 47 | return provide(); 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/example/webapp/ExampleApplication.java: -------------------------------------------------------------------------------- 1 | package org.secnod.example.webapp; 2 | 3 | import org.apache.shiro.web.jaxrs.ShiroFeature; 4 | import org.glassfish.jersey.server.ResourceConfig; 5 | import org.secnod.shiro.jersey.SubjectFactory; 6 | 7 | /** 8 | * An example JAX-RS application using Apache Shiro. 9 | */ 10 | public class ExampleApplication extends ResourceConfig { 11 | 12 | public ExampleApplication() { 13 | super(); 14 | register(ShiroFeature.class); 15 | register(new SubjectFactory()); 16 | register(new HelloWorldResource()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/org/secnod/example/webapp/HelloWorldResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.example.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | 8 | @Path("/hello-world") 9 | @Produces(MediaType.TEXT_PLAIN) 10 | public class HelloWorldResource { 11 | 12 | @GET 13 | public String get() { 14 | return "Hello, world!"; 15 | } 16 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/example/webapp/User.java: -------------------------------------------------------------------------------- 1 | package org.secnod.example.webapp; 2 | 3 | import org.apache.shiro.authz.AuthorizationException; 4 | import org.apache.shiro.authz.UnauthorizedException; 5 | import org.apache.shiro.subject.Subject; 6 | 7 | public class User { 8 | 9 | private final Subject subject; 10 | 11 | public User(Subject subject) { 12 | super(); 13 | if (subject == null) 14 | throw new NullPointerException(); 15 | this.subject = subject; 16 | } 17 | 18 | public T unwrap(Class type) { 19 | if (Subject.class.equals(type)) return type.cast(subject); 20 | 21 | throw new IllegalArgumentException("User " + this + " cannot be unwrapped to " + type); 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | String username = subject.getPrincipal() != null ? subject.getPrincipal().toString() : null; 27 | return username != null ? username : "anonymous"; 28 | } 29 | 30 | public void checkPermissionBySomeRule() throws AuthorizationException { 31 | // Apply domain specific authorization rules based on data found in the user's data, subject principals etc. 32 | if (Math.random() < 0.5) throw new UnauthorizedException(); 33 | } 34 | 35 | // Convenience delegate methods to the Subject 36 | 37 | public void checkPermission(String permission) throws AuthorizationException { 38 | subject.checkPermission(permission); 39 | } 40 | 41 | public void checkRole(String roleIdentifier) throws AuthorizationException { 42 | this.subject.checkRole(roleIdentifier); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/org/secnod/example/webapp/UserFactory.java: -------------------------------------------------------------------------------- 1 | package org.secnod.example.webapp; 2 | 3 | import org.apache.shiro.SecurityUtils; 4 | import org.secnod.shiro.jersey.TypeFactory; 5 | 6 | 7 | public class UserFactory extends TypeFactory { 8 | 9 | public UserFactory() { 10 | super(User.class); 11 | } 12 | 13 | @Override 14 | public User provide() { 15 | return new User(SecurityUtils.getSubject()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/AnnotationAuthTest.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | 9 | import javax.ws.rs.client.Client; 10 | import javax.ws.rs.client.ClientBuilder; 11 | import javax.ws.rs.client.Entity; 12 | import javax.ws.rs.client.WebTarget; 13 | import javax.ws.rs.core.Form; 14 | import javax.ws.rs.core.Response; 15 | 16 | import org.apache.http.auth.AuthScope; 17 | import org.apache.http.auth.UsernamePasswordCredentials; 18 | import org.apache.http.client.CredentialsProvider; 19 | import org.apache.http.impl.client.BasicCredentialsProvider; 20 | import org.glassfish.jersey.apache.connector.ApacheClientProperties; 21 | import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; 22 | import org.glassfish.jersey.client.ClientConfig; 23 | import org.junit.AfterClass; 24 | import org.junit.Before; 25 | import org.junit.BeforeClass; 26 | import org.junit.Test; 27 | 28 | public class AnnotationAuthTest { 29 | 30 | private static final String USER1_PASSWORD = "user1pw"; 31 | private static final String USER1 = "user1"; 32 | 33 | private String baseUrl; 34 | 35 | private static Client client; 36 | 37 | @Before 38 | public void setup() { 39 | Integer port = Integer.getInteger("org.secnod.shiro.test.port"); 40 | baseUrl = "http://localhost:" + port + "/api/"; 41 | logout(); 42 | } 43 | 44 | @BeforeClass 45 | public static void setupClass() throws Exception { 46 | client = ClientBuilder.newClient(); 47 | } 48 | 49 | @AfterClass 50 | public static void tearDownClass() { 51 | client.close(); 52 | } 53 | 54 | private WebTarget webTarget(String relativeUrl) { 55 | return client.target(baseUrl + relativeUrl); 56 | } 57 | 58 | private Client newClient(Map props) { 59 | ClientConfig config = new ClientConfig().connectorProvider(new ApacheConnectorProvider()); 60 | if (props != null) 61 | for (Entry entry : props.entrySet()) 62 | config.property(entry.getKey(), entry.getValue()); 63 | return ClientBuilder.newClient(config); 64 | } 65 | 66 | private Client newClient() { 67 | return newClient(null); 68 | } 69 | 70 | private void auth(String username, String password) { 71 | client.close(); 72 | client = null; 73 | CredentialsProvider credentials = new BasicCredentialsProvider(); 74 | credentials.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); 75 | Map props = new HashMap<>(); 76 | props.put(ApacheClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true); 77 | props.put(ApacheClientProperties.CREDENTIALS_PROVIDER, credentials); 78 | client = newClient(props); 79 | } 80 | 81 | private void loginUser() { 82 | auth(USER1, USER1_PASSWORD); 83 | } 84 | 85 | private void loginSuperUser() { 86 | auth("user2", "user2pw"); 87 | } 88 | 89 | private void logout() { 90 | client.close(); 91 | client = null; 92 | client = newClient(); 93 | } 94 | 95 | private void assertGetStatus(int expectedStatus, WebTarget t) { 96 | assertStatus(expectedStatus, "GET", t, null); 97 | } 98 | 99 | private void assertPutStatus(int expectedStatus, WebTarget t) { 100 | assertStatus(expectedStatus, "PUT", t, "dummy entity"); 101 | } 102 | 103 | private void assertStatus(int expectedStatus, String method, WebTarget t, String entity) { 104 | Response response = entity != null ? t.request().method(method, Entity.text(entity)) 105 | : t.request().method(method); 106 | int status = response.getStatus(); 107 | response.close(); 108 | assertEquals("Unexpected HTTP status for " + method + " " + t, expectedStatus, status); 109 | } 110 | 111 | @Test 112 | public void publicAccess() { 113 | assertGetStatus(200, webTarget("public")); 114 | loginUser(); 115 | assertGetStatus(200, webTarget("public")); 116 | } 117 | 118 | private void protectedReadWrite(String resourcePath) { 119 | assertGetStatus(401, webTarget(resourcePath)); 120 | loginUser(); 121 | assertGetStatus(200, webTarget(resourcePath)); 122 | assertPutStatus(403, webTarget(resourcePath)); 123 | loginSuperUser(); 124 | assertGetStatus(200, webTarget(resourcePath)); 125 | assertPutStatus(200, webTarget(resourcePath)); 126 | } 127 | 128 | @Test 129 | public void protectedByPermission() { 130 | protectedReadWrite("protected/permission"); 131 | } 132 | 133 | @Test 134 | public void protectedByRole() { 135 | protectedReadWrite("protected/role"); 136 | } 137 | 138 | @Test 139 | public void protectedOnlyUsers() { 140 | assertGetStatus(401, webTarget("protected/user")); 141 | loginUser(); 142 | assertGetStatus(200, webTarget("protected/user")); 143 | } 144 | 145 | @Test 146 | public void guestOnly() { 147 | assertGetStatus(200, webTarget("guestonly")); 148 | loginUser(); 149 | assertGetStatus(401, webTarget("guestonly")); 150 | } 151 | 152 | void rememberMeUserSession() { 153 | WebTarget sessionResource = webTarget("session"); 154 | Form form = new Form() 155 | .param("username", USER1) 156 | .param("password", USER1_PASSWORD) 157 | .param("rememberMe", "true"); 158 | Response response = sessionResource.request().post(Entity.form(form)); 159 | int loginStatus = response.getStatus(); 160 | response.close(); 161 | assertEquals(200, loginStatus); 162 | } 163 | 164 | void invalidateSession() { 165 | WebTarget sessionResource = webTarget("session"); 166 | Response response = sessionResource.request().delete(); 167 | int logoutStatus = response.getStatus(); 168 | response.close(); 169 | assertEquals(200, logoutStatus); 170 | } 171 | 172 | @Test 173 | public void noRememberMe() { 174 | assertGetStatus(401, webTarget("protected/noRememberMe")); 175 | loginUser(); 176 | assertGetStatus(200, webTarget("protected/noRememberMe")); 177 | logout(); 178 | rememberMeUserSession(); 179 | assertGetStatus(200, webTarget("protected/noRememberMe")); 180 | invalidateSession(); 181 | assertGetStatus(401, webTarget("protected/noRememberMe")); 182 | assertGetStatus(200, webTarget("protected/permission")); 183 | } 184 | 185 | @Test 186 | public void userAndSubjectInjection() { 187 | assertGetStatus(200, webTarget("inject/usersubject")); 188 | loginUser(); 189 | assertGetStatus(200, webTarget("inject/usersubject")); 190 | } 191 | 192 | @Test 193 | public void fieldInjectionFails() { 194 | assertGetStatus(500, webTarget("inject/field")); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/IntegrationTestSuite.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration; 2 | 3 | import org.eclipse.jetty.server.Server; 4 | import org.junit.ClassRule; 5 | import org.junit.rules.ExternalResource; 6 | import org.junit.runner.RunWith; 7 | import org.junit.runners.Suite; 8 | import org.junit.runners.Suite.SuiteClasses; 9 | import org.secnod.shiro.test.integration.webapp.JettyServer; 10 | 11 | @RunWith(Suite.class) 12 | @SuiteClasses({AnnotationAuthTest.class}) 13 | public class IntegrationTestSuite { 14 | 15 | @ClassRule 16 | public static ExternalResource resource = new ExternalResource() { 17 | Server server; 18 | @Override 19 | protected void before() throws Throwable { 20 | int port = JettyServer.allocatePort(); 21 | System.setProperty("org.secnod.shiro.test.port", Integer.toString(port)); 22 | server = JettyServer.start(port); 23 | }; 24 | 25 | @Override 26 | protected void after() { 27 | try { 28 | server.stop(); 29 | } catch (Exception e) { 30 | throw new RuntimeException(e); 31 | } 32 | }; 33 | }; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/FieldInjectionResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.WebApplicationException; 7 | import javax.ws.rs.core.MediaType; 8 | import javax.ws.rs.core.Response.Status; 9 | 10 | import org.apache.shiro.subject.Subject; 11 | import org.secnod.shiro.jaxrs.Auth; 12 | 13 | @Path("/inject/field") 14 | @Produces(MediaType.TEXT_PLAIN) 15 | public class FieldInjectionResource { 16 | 17 | @Auth Subject subject; 18 | 19 | @GET 20 | public String sessionUser() { 21 | try { 22 | return subject.getPrincipal().toString(); 23 | } catch (NullPointerException e) { 24 | throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/GuestOnlyResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | 8 | import org.apache.shiro.authz.annotation.RequiresGuest; 9 | 10 | @Path("/guestonly") 11 | @Produces(MediaType.TEXT_PLAIN) 12 | @RequiresGuest 13 | public class GuestOnlyResource { 14 | 15 | @GET 16 | public String get() { 17 | return "guest only"; 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/InjectionResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.WebApplicationException; 7 | import javax.ws.rs.core.MediaType; 8 | import javax.ws.rs.core.Response.Status; 9 | 10 | import org.apache.shiro.subject.Subject; 11 | import org.secnod.example.webapp.User; 12 | import org.secnod.shiro.jaxrs.Auth; 13 | 14 | @Path("/inject") 15 | @Produces(MediaType.TEXT_PLAIN) 16 | public class InjectionResource { 17 | 18 | @Path("usersubject") 19 | @GET 20 | public String sessionUser(@Auth Subject subject, @Auth User user) { 21 | if (subject != user.unwrap(Subject.class)) { 22 | throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); 23 | } 24 | return "User and Subject method param injection works.\n"; 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/IntegrationTestApplication.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import org.apache.shiro.web.jaxrs.ShiroFeature; 9 | import org.glassfish.jersey.server.ResourceConfig; 10 | import org.secnod.example.webapp.UserFactory; 11 | import org.secnod.shiro.jaxrs.ShiroExceptionMapper; 12 | import org.secnod.shiro.jersey.AuthorizationFilterFeature; 13 | import org.secnod.shiro.jersey.SubjectFactory; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; 18 | 19 | /** 20 | * A JAX-RS application for running the integration tests. 21 | */ 22 | @SuppressWarnings("deprecation") 23 | public class IntegrationTestApplication extends ResourceConfig { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(IntegrationTestApplication.class); 26 | 27 | public IntegrationTestApplication() { 28 | super(); 29 | if (Boolean.getBoolean("shiro.jersey")) { 30 | log.info("Using the shiro-jersey feature."); 31 | register(new AuthorizationFilterFeature()); 32 | register(new ShiroExceptionMapper()); 33 | } else { 34 | log.info("Using the native shiro-jaxrs feature."); 35 | register(ShiroFeature.class); 36 | } 37 | register(new SubjectFactory()); 38 | register(new UserFactory()); 39 | register(new JacksonJsonProvider()); 40 | for (Object resource : createAllIntegrationTestResources()) { 41 | register(resource); 42 | } 43 | for (Class resource : allIntegrationTestResourceClasses()) { 44 | register(resource); 45 | } 46 | } 47 | 48 | public static Set createAllIntegrationTestResources() { 49 | return new HashSet(Arrays.asList( 50 | new PublicResource(), 51 | new GuestOnlyResource(), 52 | new PermissionProtectedResource(), 53 | new RoleProtectedResource(), 54 | new RequireUserResource(), 55 | new RequireAuthenticatedUserResource(), 56 | new SessionResource(), 57 | new SubjectAuthResource(), 58 | new UserAuthResource(), 59 | new InjectionResource() 60 | )); 61 | } 62 | 63 | public static Set> allIntegrationTestResourceClasses() { 64 | return Collections.singleton(FieldInjectionResource.class); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/JettyServer.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import java.io.IOException; 4 | import java.net.ServerSocket; 5 | 6 | import org.eclipse.jetty.server.Server; 7 | import org.eclipse.jetty.util.resource.Resource; 8 | import org.eclipse.jetty.webapp.WebAppContext; 9 | 10 | /** 11 | * An example standalone Jetty server. 12 | */ 13 | public class JettyServer { 14 | 15 | public static void main(String[] args) throws Exception { 16 | start(8080).join(); 17 | } 18 | 19 | public static Server start(int port) throws Exception { 20 | Server server = new Server(port); 21 | 22 | WebAppContext webapp = new WebAppContext(); 23 | webapp.setContextPath("/"); 24 | String resourcePath = JettyServer.class.getPackage().getName().replace('.', '/'); 25 | webapp.setBaseResource(Resource.newClassPathResource(resourcePath)); 26 | webapp.setParentLoaderPriority(true); 27 | 28 | server.setHandler(webapp); 29 | server.setStopTimeout(5000); 30 | server.start(); 31 | return server; 32 | } 33 | 34 | public static int allocatePort() throws IOException { 35 | try (ServerSocket s = new ServerSocket(0)) { 36 | return s.getLocalPort(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/OptionalBasicHttpAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.servlet.ServletRequest; 4 | import javax.servlet.ServletResponse; 5 | 6 | import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; 7 | 8 | /** 9 | * Filter for optional HTTP Basic login for Apache Shiro. 10 | * Only sends the challenge response if a login attempt fails. 11 | */ 12 | public class OptionalBasicHttpAuthenticationFilter extends BasicHttpAuthenticationFilter { 13 | 14 | @Override 15 | protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { 16 | if (!isLoginAttempt(request, response)) return true; 17 | if (executeLogin(request, response)) return true; 18 | else return sendChallenge(request, response); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/PermissionProtectedResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import java.util.concurrent.atomic.AtomicReference; 4 | 5 | import javax.ws.rs.GET; 6 | import javax.ws.rs.PUT; 7 | import javax.ws.rs.Path; 8 | import javax.ws.rs.Produces; 9 | import javax.ws.rs.core.MediaType; 10 | 11 | import org.apache.shiro.authz.annotation.RequiresPermissions; 12 | 13 | @Path("/protected/permission") 14 | @Produces(MediaType.TEXT_PLAIN) 15 | @RequiresPermissions("protected:read") 16 | public class PermissionProtectedResource { 17 | 18 | private AtomicReference value = new AtomicReference("a permission protected resource"); 19 | 20 | @GET 21 | public String get() { 22 | return value.get(); 23 | } 24 | 25 | @PUT 26 | @RequiresPermissions("protected:write") 27 | public String set(String newValue) { 28 | value.set(newValue); 29 | return newValue; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/PublicResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | 8 | @Path("/public") 9 | @Produces(MediaType.TEXT_PLAIN) 10 | public class PublicResource { 11 | 12 | @GET 13 | public String get() { 14 | return "a public resource"; 15 | } 16 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/RequireAuthenticatedUserResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | 8 | import org.apache.shiro.authz.annotation.RequiresAuthentication; 9 | 10 | @Path("/protected/noRememberMe") 11 | @Produces(MediaType.TEXT_PLAIN) 12 | @RequiresAuthentication 13 | public class RequireAuthenticatedUserResource { 14 | 15 | @GET 16 | public String get() { 17 | return "for non anonymous user users only, remember me not allowed"; 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/RequireUserResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | 8 | import org.apache.shiro.authz.annotation.RequiresUser; 9 | 10 | @Path("/protected/user") 11 | @Produces(MediaType.TEXT_PLAIN) 12 | @RequiresUser 13 | public class RequireUserResource { 14 | 15 | @GET 16 | public String get() { 17 | return "for non-anonymous users only"; 18 | } 19 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/RoleProtectedResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import java.util.concurrent.atomic.AtomicReference; 4 | 5 | import javax.ws.rs.GET; 6 | import javax.ws.rs.PUT; 7 | import javax.ws.rs.Path; 8 | import javax.ws.rs.Produces; 9 | import javax.ws.rs.core.MediaType; 10 | 11 | import org.apache.shiro.authz.annotation.RequiresRoles; 12 | 13 | @Path("/protected/role") 14 | @Produces(MediaType.TEXT_PLAIN) 15 | @RequiresRoles("user") 16 | public class RoleProtectedResource { 17 | 18 | private AtomicReference value = new AtomicReference("a role protected resource"); 19 | 20 | @GET 21 | public String get() { 22 | return value.get(); 23 | } 24 | 25 | @PUT 26 | @RequiresRoles("superuser") 27 | public String set(String newValue) { 28 | value.set(newValue); 29 | return newValue; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/SessionResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.servlet.http.HttpSession; 5 | import javax.ws.rs.DELETE; 6 | import javax.ws.rs.DefaultValue; 7 | import javax.ws.rs.FormParam; 8 | import javax.ws.rs.GET; 9 | import javax.ws.rs.POST; 10 | import javax.ws.rs.Path; 11 | import javax.ws.rs.Produces; 12 | import javax.ws.rs.WebApplicationException; 13 | import javax.ws.rs.core.Context; 14 | import javax.ws.rs.core.MediaType; 15 | import javax.ws.rs.core.Response.Status; 16 | 17 | import org.apache.shiro.SecurityUtils; 18 | import org.apache.shiro.authc.UsernamePasswordToken; 19 | import org.secnod.example.webapp.User; 20 | import org.secnod.shiro.jaxrs.Auth; 21 | 22 | @Path("/session") 23 | @Produces(MediaType.TEXT_PLAIN) 24 | public class SessionResource { 25 | 26 | @POST 27 | public String login(@FormParam("username") String username, @FormParam("password") String password, 28 | @FormParam("rememberMe") @DefaultValue("false") boolean rembemberMe) { 29 | SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password.toCharArray(), rembemberMe)); 30 | return "Logged in as " + username + "\n"; 31 | } 32 | 33 | @GET 34 | public String sessionUser(@Auth User user) { 35 | return "Current user: " + user + "\n"; 36 | } 37 | 38 | /** 39 | * Invalidate the session without logging out the Shiro subject. For testing the remember me token. 40 | */ 41 | @DELETE 42 | public String invalidateHttpSession(@Context HttpServletRequest request) { 43 | HttpSession session = request.getSession(false); 44 | if (session == null) throw new WebApplicationException(Status.BAD_REQUEST); 45 | 46 | session.invalidate(); 47 | return "session invalidated"; 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/SubjectAuthResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | 8 | import org.apache.shiro.authz.UnauthenticatedException; 9 | import org.apache.shiro.authz.annotation.RequiresPermissions; 10 | import org.apache.shiro.subject.Subject; 11 | import org.secnod.shiro.jaxrs.Auth; 12 | 13 | // TODO Delete? 14 | 15 | @Path("/auth/subject") 16 | @Produces(MediaType.TEXT_PLAIN) 17 | @RequiresPermissions("protected:read") 18 | public class SubjectAuthResource { 19 | 20 | @GET 21 | public String get(@Auth Subject subject) { 22 | if (!subject.isAuthenticated()) throw new UnauthenticatedException(); 23 | 24 | return Double.toString(Math.random()); 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/java/org/secnod/shiro/test/integration/webapp/UserAuthResource.java: -------------------------------------------------------------------------------- 1 | package org.secnod.shiro.test.integration.webapp; 2 | 3 | import javax.ws.rs.GET; 4 | import javax.ws.rs.Path; 5 | import javax.ws.rs.Produces; 6 | import javax.ws.rs.core.MediaType; 7 | 8 | import org.apache.shiro.authz.annotation.RequiresPermissions; 9 | import org.secnod.example.webapp.User; 10 | import org.secnod.shiro.jaxrs.Auth; 11 | 12 | //TODO Delete? 13 | 14 | @Path("/auth/user") 15 | @Produces(MediaType.TEXT_PLAIN) 16 | @RequiresPermissions("protected:read") 17 | public class UserAuthResource { 18 | 19 | @GET 20 | public String get(@Auth User user) { 21 | user.checkPermissionBySomeRule(); 22 | return Double.toString(Math.random()); 23 | } 24 | } -------------------------------------------------------------------------------- /src/test/resources/logging.properties: -------------------------------------------------------------------------------- 1 | handlers = java.util.logging.ConsoleHandler 2 | .level = SEVERE 3 | java.util.logging.ConsoleHandler.level = FINE 4 | 5 | #org.apache.shiro.level = FINE 6 | #org.eclipse.jetty.level = FINE 7 | -------------------------------------------------------------------------------- /src/test/resources/org/secnod/example/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | rest-application 6 | org.glassfish.jersey.servlet.ServletContainer 7 | 8 | javax.ws.rs.Application 9 | org.secnod.example.webapp.ExampleApplication 10 | 11 | 12 | 13 | 14 | rest-application 15 | /api/* 16 | 17 | 18 | 19 | shiroConfigLocations 20 | classpath:shiro.ini 21 | 22 | 23 | 24 | org.apache.shiro.web.env.EnvironmentLoaderListener 25 | 26 | 27 | 28 | ShiroFilter 29 | org.apache.shiro.web.servlet.ShiroFilter 30 | 31 | 32 | 33 | ShiroFilter 34 | /* 35 | REQUEST 36 | FORWARD 37 | INCLUDE 38 | ERROR 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/test/resources/org/secnod/shiro/test/integration/webapp/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 404 Not found, custom error page configured in web.xml 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/test/resources/org/secnod/shiro/test/integration/webapp/WEB-INF/shiro.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | optionalAuthcBasic = org.secnod.shiro.test.integration.webapp.OptionalBasicHttpAuthenticationFilter 3 | 4 | [users] 5 | admin = adminpw, admin 6 | user1 = user1pw, user 7 | user2 = user2pw, user, superuser 8 | 9 | [roles] 10 | admin = * 11 | user = protected:read 12 | superuser = protected:* 13 | 14 | 15 | [urls] 16 | /api/session/** = anon 17 | /** = noSessionCreation, optionalAuthcBasic, anon 18 | 19 | -------------------------------------------------------------------------------- /src/test/resources/org/secnod/shiro/test/integration/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | rest-application 6 | org.glassfish.jersey.servlet.ServletContainer 7 | 8 | javax.ws.rs.Application 9 | org.secnod.shiro.test.integration.webapp.IntegrationTestApplication 10 | 11 | 12 | 13 | 14 | rest-application 15 | /api/* 16 | 17 | 18 | 19 | shiroConfigLocations 20 | classpath:org/secnod/shiro/test/integration/webapp/WEB-INF/shiro.ini 21 | 22 | 23 | 24 | org.apache.shiro.web.env.EnvironmentLoaderListener 25 | 26 | 27 | 28 | ShiroFilter 29 | org.apache.shiro.web.servlet.IniShiroFilter 30 | 31 | 32 | 33 | ShiroFilter 34 | /* 35 | REQUEST 36 | FORWARD 37 | INCLUDE 38 | ERROR 39 | 40 | 41 | 42 | 404 43 | /404.html 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/test/resources/org/secnod/shiro/test/integration/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Hello World!

9 | 10 |
11 | 12 | 13 |
14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/resources/shiro.ini: -------------------------------------------------------------------------------- 1 | [main] 2 | 3 | [users] 4 | exampleuser = examplepassword, examplerole 5 | 6 | [roles] 7 | examplerole = something:readpermission 8 | 9 | [urls] 10 | /** = noSessionCreation, authcBasic 11 | --------------------------------------------------------------------------------