├── .gitattributes ├── .github ├── settings.xml └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pom.xml ├── renovate.json └── src ├── main └── java │ └── org │ └── dhatim │ └── dropwizard │ └── jwt │ └── cookie │ └── authentication │ ├── CurrentPrincipal.java │ ├── DefaultJwtCookiePrincipal.java │ ├── DontRefreshSession.java │ ├── DontRefreshSessionFilter.java │ ├── JwtCookieAuthBundle.java │ ├── JwtCookieAuthConfiguration.java │ ├── JwtCookieAuthRequestFilter.java │ ├── JwtCookieAuthResponseFilter.java │ ├── JwtCookiePrincipal.java │ ├── JwtCookiePrincipalAuthenticator.java │ ├── JwtCookieSecurityContext.java │ └── SameSite.java └── test └── java └── org └── dhatim └── dropwizard └── jwt └── cookie └── authentication ├── JwtCookieAuthenticationTest.java ├── TestApplication.java └── TestResource.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ossrh 5 | ${env.SONATYPE_USERNAME} 6 | ${env.SONATYPE_PASSWORD} 7 | 8 | 9 | 10 | 11 | ossrh 12 | 13 | true 14 | 15 | 16 | ${env.GPG_PASSPHRASE} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: [ '*' ] 5 | tags: [ '*' ] 6 | pull_request: 7 | branches: [ '*' ] 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-java@v4 14 | with: 15 | distribution: 'zulu' 16 | java-version: '11' 17 | - name: maven build 18 | env: 19 | GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} 20 | GPG_OWNERTRUST: ${{ secrets.GPG_OWNERTRUST }} 21 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 22 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 23 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 24 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 25 | run: | 26 | if echo "${GITHUB_REF_NAME}" | egrep '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$' 27 | then 28 | # the tag looks like a version number: proceed with release 29 | echo ${GPG_SECRET_KEY} | base64 --decode | gpg --import --no-tty --batch --yes 30 | echo ${GPG_OWNERTRUST} | base64 --decode | gpg --import-ownertrust --no-tty --batch --yes 31 | mvn -ntp versions:set -DnewVersion=${GITHUB_REF_NAME} 32 | mvn -ntp -s .github/settings.xml -Prelease deploy jacoco:report coveralls:report -DrepoToken=${COVERALLS_TOKEN} 33 | else 34 | # this is a regular build 35 | mvn -ntp install 36 | fi 37 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '21 5 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 28 | node_modules 29 | 30 | # ========================= 31 | # Operating System Files 32 | # ========================= 33 | 34 | # OSX 35 | # ========================= 36 | 37 | .DS_Store 38 | .AppleDouble 39 | .LSOverride 40 | 41 | # Thumbnails 42 | ._* 43 | 44 | # Files that might appear in the root of a volume 45 | .DocumentRevisions-V100 46 | .fseventsd 47 | .Spotlight-V100 48 | .TemporaryItems 49 | .Trashes 50 | .VolumeIcon.icns 51 | 52 | # Directories potentially created on remote AFP share 53 | .AppleDB 54 | .AppleDesktop 55 | Network Trash Folder 56 | Temporary Items 57 | .apdisk 58 | 59 | # Windows 60 | # ========================= 61 | 62 | # Windows image file caches 63 | Thumbs.db 64 | ehthumbs.db 65 | 66 | # Folder config file 67 | Desktop.ini 68 | 69 | # Recycle Bin used on file shares 70 | $RECYCLE.BIN/ 71 | 72 | # Windows Installer files 73 | *.cab 74 | *.msi 75 | *.msm 76 | *.msp 77 | 78 | # Windows shortcuts 79 | *.lnk 80 | /target/ 81 | 82 | # Eclipse env files 83 | *.project 84 | *.settings 85 | *.classpath 86 | *.class 87 | bin 88 | /.idea/ 89 | /dropwizard-jwt-cookie-authentication.iml 90 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [3.1.0] - 2017-09-07 4 | 5 | - [ADDED] `CurrentPrincipal.get` accessor 6 | 7 | ## [3.0.0] - 2017-02-23 8 | 9 | - [ADDED] `httpOnly` flag configuration 10 | - [CHANGED] dependency to dropwizard `1.1.0` 11 | 12 | ### BC breaks: 13 | - `httpsOnly` config parameter renamed into `secure` 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Dhatim 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/dhatim/dropwizard-jwt-cookie-authentication/workflows/build/badge.svg)](https://github.com/dhatim/dropwizard-jwt-cookie-authentication/actions) 2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.dhatim/dropwizard-jwt-cookie-authentication/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.dhatim/dropwizard-jwt-cookie-authentication) 3 | [![Coverage Status](https://coveralls.io/repos/github/dhatim/dropwizard-jwt-cookie-authentication/badge.svg?branch=master)](https://coveralls.io/github/dhatim/dropwizard-jwt-cookie-authentication?branch=master) 4 | [![Javadoc](https://www.javadoc.io/badge/org.dhatim/dropwizard-jwt-cookie-authentication.svg)](http://www.javadoc.io/doc/org.dhatim/dropwizard-jwt-cookie-authentication) 5 | [![Mentioned in Awesome Dropwizard](https://awesome.re/mentioned-badge.svg)](https://github.com/stve/awesome-dropwizard) 6 | 7 | **Please note version 5 requires Java 11 and Dropwizard 4.** 8 | 9 | # dropwizard-jwt-cookie-authentication 10 | 11 | Statelessness is not only an architectural constaint of RESTful applications, it also comes with a lot of advantages regarding scalability and memory usage. 12 | 13 | A common pattern is to provide the client with a signed JWT containing all necessary authorization and/or session state information. This JWT must then be passed along subsequent requests, usually in bearer Authorization HTTP headers. 14 | 15 | However, in the particular case where clients of the RESTful application are web applications, it is much more interesting to use cookies. The browser will automatically read, store, send and expire the tokens, saving front-end developers the hassle of doing it themselves. 16 | 17 | This dropwizard bundle makes things simple for back-end developpers too. It automatically serializes/deserializes session information into/from JWT cookies. 18 | 19 | ## Enabling the bundle 20 | 21 | ### Add the dropwizard-jwt-cookie-authentication dependency 22 | 23 | Add the dropwizard-jwt-cookie-authentication library as a dependency to your `pom.xml` file: 24 | 25 | ```xml 26 | 27 | org.dhatim 28 | dropwizard-jwt-cookie-authentication 29 | 5.1.3 30 | 31 | ``` 32 | 33 | ### Edit you app's Dropwizard YAML config file 34 | 35 | The default values are shown below. If they suit you, this step is optional. 36 | 37 | ```yml 38 | jwtCookieAuth: 39 | secretSeed: null 40 | secure: false 41 | httpOnly: true 42 | domain: null 43 | sameSite: null 44 | sessionExpiryVolatile: PT30m 45 | sessionExpiryPersistent: P7d 46 | ``` 47 | 48 | ### Add the 'JwtCookieAuthConfiguration' to your application configuration class: 49 | 50 | This step is also optional if you skipped the previous one. 51 | 52 | ```java 53 | @Valid 54 | @NotNull 55 | private JwtCookieAuthConfiguration jwtCookieAuth = new JwtCookieAuthConfiguration(); 56 | 57 | public JwtCookieAuthConfiguration getJwtCookieAuth() { 58 | return jwtCookieAuth; 59 | } 60 | ``` 61 | 62 | ### Add the bundle to the dropwizard application 63 | 64 | ```java 65 | public void initialize(Bootstrap bootstrap) { 66 | bootstrap.addBundle(JwtCookieAuthBundle.getDefault()); 67 | } 68 | ``` 69 | 70 | If you have a custom configuration fot the bundle, specify it like so: 71 | ```java 72 | bootstrap.addBundle(JwtCookieAuthBundle.getDefault().withConfigurationSupplier(MyAppConfiguration::getJwtCookieAuth)); 73 | ``` 74 | 75 | ## Using the bundle 76 | 77 | By default, the JWT cookie is serialized from / deserialized in an instance of [`DefaultJwtCookiePrincipal`](http://static.javadoc.io/org.dhatim/dropwizard-jwt-cookie-authentication/3.0.0/org/dhatim/dropwizard/jwt/cookie/authentication/DefaultJwtCookiePrincipal.html). 78 | 79 | When the user authenticate, you must put an instance of `DefaultJwtCookiePrincipal` in the security context (which you can inject in your resources using the `@Context` annotation) using `JwtCookiePrincipal.addInContext` 80 | ```java 81 | JwtCookiePrincipal principal = new DefaultJwtCookiePrincipal(name); 82 | principal.addInContext(context); 83 | ``` 84 | 85 | Once a principal has been set, it can be retrieved using the `@Auth` annotation in method signatures. You can also use `CurrentPrincipal.get()` within the request thread. 86 | 87 | Each time an API endpoint is called, a fresh cookie JWT is issued to reset the session TTL. You can use the `@DontRefreshSession` on methods where this behavior is unwanted. 88 | 89 | To specify a max age in the cookie (aka "remember me"), use `DefaultJwtCookiePrincipal.setPersistent(true)`. 90 | 91 | It is a stateless auhtentication method, so there is no real way to invalidate a session other than waiting for the JWT to expire. However calling `JwtCookiePrincipal.removeFromContext(context)` will make browsers discard the cookie by setting the cookie expiration to a past date. 92 | 93 | Principal roles can be specified via the `DefaultJwtCookiePrincipal.setRoles(...)` method. You can then define fine grained access control using annotations such as `@RolesAllowed` or `@PermitAll`. 94 | 95 | Additional custom data can be stored in the Principal using `DefaultJwtCookiePrincipal.getClaims().put(key, value)`. 96 | 97 | ## Sample application resource 98 | ```java 99 | @POST 100 | @Consumes(MediaType.APPLICATION_JSON) 101 | @Produces(MediaType.APPLICATION_JSON) 102 | public DefaultJwtCookiePrincipal login(@Context ContainerRequestContext requestContext, String name){ 103 | DefaultJwtCookiePrincipal principal = new DefaultJwtCookiePrincipal(name); 104 | principal.addInContext(requestContext); 105 | return principal; 106 | } 107 | 108 | @GET 109 | @Path("logout") 110 | public void logout(@Context ContainerRequestContext requestContext){ 111 | JwtCookiePrincipal.removeFromContext(requestContext); 112 | } 113 | 114 | @GET 115 | @Produces(MediaType.APPLICATION_JSON) 116 | public DefaultJwtCookiePrincipal getPrincipal(@Auth DefaultJwtCookiePrincipal principal){ 117 | return principal; 118 | } 119 | 120 | @GET 121 | @Path("idempotent") 122 | @Produces(MediaType.APPLICATION_JSON) 123 | @DontRefreshSession 124 | public DefaultJwtCookiePrincipal getSubjectWithoutRefreshingSession(@Auth DefaultJwtCookiePrincipal principal){ 125 | return principal; 126 | } 127 | 128 | @GET 129 | @Path("restricted") 130 | @RolesAllowed("admin") 131 | public String getRestrictedResource(){ 132 | return "SuperSecretStuff"; 133 | } 134 | ``` 135 | 136 | ## Custom principal implementation 137 | 138 | If you want to use your own Principal class instead of the `DefaultJwtCookiePrincipal`, simply implement the interface `JwtCookiePrincipal` and pass it to the bundle constructor along with functions to serialize it into / deserialize it from JWT claims. 139 | 140 | e.g: 141 | 142 | ```java 143 | bootstrap.addBundle(new JwtCookieAuthBundle<>(MyCustomPrincipal.class, MyCustomPrincipal::toClaims, MyCustomPrincipal::new)); 144 | ``` 145 | 146 | ## JWT Signing Key 147 | 148 | By default, the signing key is randomly generated on application startup. It means that users will have to re-authenticate after each server reboot. 149 | 150 | To avoid this, you can specify a `secretSeed` in the configuration. This seed will be used to generate the signing key, which will therefore be the same at each application startup. 151 | 152 | Alternatively you can specify your own key factory: 153 | ```java 154 | bootstrap.addBundle(JwtCookieAuthBundle.getDefault().withKeyProvider((configuration, environment) -> {/*return your own key*/})); 155 | ``` 156 | ## Manual Setup 157 | 158 | If you need [Chained Factories](https://www.dropwizard.io/en/latest/manual/auth.html#chained-factories) or [Multiple Principals and Authenticators](https://www.dropwizard.io/en/latest/manual/auth.html#multiple-principals-and-authenticators), don't register directly the bundle. Use instead its `getAuthRequestFilter` and `getAuthResponseFilter` methods to manually setup authentication. 159 | 160 | You will also be responsible for generating the signing key and registering `RolesAllowedDynamicFeature` or `DontRefreshSessionFilter` if they are needed. 161 | 162 | Example: 163 | 164 | ```java 165 | JwtCookieAuthBundle jwtCookieAuthBundle = new JwtCookieAuthBundle<>( 166 | MyJwtCookiePrincipal.class, 167 | MyJwtCookiePrincipal::toClaims, 168 | MyJwtCookiePrincipal::new); 169 | 170 | SecretKey key = JwtCookieAuthBundle.generateKey(configuration.getJwtCookieAuth().getSecretSeed()); 171 | 172 | environment.jersey().register( 173 | new PolymorphicAuthDynamicFeature<>( 174 | ImmutableMap.of( 175 | MyJwtCookiePrincipal.class, jwtCookieAuthBundle.getAuthRequestFilter(key), 176 | MyBasicPrincipal.class, new BasicCredentialAuthFilter.Builder() 177 | .setAuthenticator(new MyBasicAuthenticator()) 178 | .setRealm("SUPER SECRET STUFF") 179 | .buildAuthFilter() 180 | ) 181 | ) 182 | ); 183 | environment.jersey().register(new PolymorphicAuthValueFactoryProvider.Binder<>(ImmutableSet.of(MyJwtCookiePrincipal.class, MyBasicPrincipal.class))); 184 | environment.jersey().register(RolesAllowedDynamicFeature.class); 185 | environment.jersey().register(DontRefreshSessionFilter.class); 186 | environment.jersey().register(jwtCookieAuthBundle.getAuthResponseFilter(key, configuration.getJwtCookieAuth())); 187 | ``` 188 | 189 | ## Javadoc 190 | 191 | It's [here](http://www.javadoc.io/doc/org.dhatim/dropwizard-jwt-cookie-authentication). 192 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.dhatim 7 | dropwizard-jwt-cookie-authentication 8 | 0-SNAPSHOT 9 | jar 10 | ${project.groupId}:${project.artifactId} 11 | Dropwizard bundle managing authentication through JWT cookies 12 | https://github.com/dhatim/dropwizard-jwt-cookie-authentication 13 | 14 | 15 | The Apache License, Version 2.0 16 | http://www.apache.org/licenses/LICENSE-2.0.txt 17 | 18 | 19 | 20 | 21 | Maxime Suret 22 | msuret@dhatim.com 23 | Dhatim 24 | http://dhatim.com/ 25 | 26 | 27 | 28 | scm:git:git@github.com:dhatim/dropwizard-jwt-cookie-authentication.git 29 | scm:git:git@github.com:dhatim/dropwizard-jwt-cookie-authentication.git 30 | 31 | git@github.com:dhatim/dropwizard-jwt-cookie-authentication.git 32 | 33 | 34 | 11 35 | 11 36 | 11 37 | UTF-8 38 | 0.12.6 39 | false 40 | 41 | 42 | 43 | ossrh 44 | https://oss.sonatype.org/content/repositories/snapshots 45 | 46 | 47 | 48 | 49 | sonatype-nexus-snapshots 50 | Sonatype Nexus Snapshots 51 | https://oss.sonatype.org/content/repositories/snapshots 52 | 53 | false 54 | 55 | 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | io.dropwizard 64 | dropwizard-bom 65 | 4.0.14 66 | pom 67 | import 68 | 69 | 70 | 71 | 72 | 73 | jakarta.annotation 74 | jakarta.annotation-api 75 | 3.0.0 76 | 77 | 78 | io.dropwizard 79 | dropwizard-core 80 | 81 | 82 | io.dropwizard 83 | dropwizard-auth 84 | 85 | 86 | io.jsonwebtoken 87 | jjwt-api 88 | ${jjwt.version} 89 | 90 | 91 | io.jsonwebtoken 92 | jjwt-impl 93 | ${jjwt.version} 94 | 95 | 96 | io.jsonwebtoken 97 | jjwt-jackson 98 | ${jjwt.version} 99 | 100 | 101 | io.dropwizard 102 | dropwizard-testing 103 | test 104 | 105 | 106 | io.dropwizard 107 | dropwizard-client 108 | test 109 | 110 | 111 | 112 | 113 | 114 | org.jacoco 115 | jacoco-maven-plugin 116 | 0.8.13 117 | 118 | 119 | 120 | prepare-agent 121 | 122 | 123 | 124 | 125 | 126 | maven-surefire-plugin 127 | 3.5.3 128 | 129 | 130 | org.eluder.coveralls 131 | coveralls-maven-plugin 132 | 4.3.0 133 | 134 | 135 | javax.xml.bind 136 | jaxb-api 137 | 2.3.1 138 | 139 | 140 | 141 | 142 | maven-source-plugin 143 | 3.3.1 144 | 145 | 146 | attach-sources 147 | 148 | jar-no-fork 149 | 150 | 151 | 152 | 153 | 154 | maven-javadoc-plugin 155 | 3.11.2 156 | 157 | 158 | attach-javadocs 159 | 160 | jar 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | release 171 | 172 | 173 | 174 | maven-gpg-plugin 175 | 176 | 177 | 178 | --pinentry-mode 179 | loopback 180 | 181 | 182 | 183 | 184 | sign-artifacts 185 | 186 | sign 187 | 188 | verify 189 | 190 | 191 | 192 | 193 | org.sonatype.plugins 194 | nexus-staging-maven-plugin 195 | true 196 | 197 | ossrh 198 | https://oss.sonatype.org 199 | true 200 | 60 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchUpdateTypes": ["minor", "patch"], 9 | "automerge": true 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/CurrentPrincipal.java: -------------------------------------------------------------------------------- 1 | package org.dhatim.dropwizard.jwt.cookie.authentication; 2 | 3 | import java.security.Principal; 4 | 5 | public class CurrentPrincipal { 6 | 7 | private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>(); 8 | 9 | protected static void set(Principal principal){ 10 | THREAD_LOCAL.set(principal); 11 | } 12 | 13 | protected static void remove(){ 14 | THREAD_LOCAL.remove(); 15 | } 16 | 17 | public static

P get() { 18 | return (P)THREAD_LOCAL.get(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/DefaultJwtCookiePrincipal.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | import io.jsonwebtoken.Claims; 20 | import io.jsonwebtoken.ClaimsBuilder; 21 | import io.jsonwebtoken.Jwts; 22 | 23 | import java.util.Collection; 24 | import java.util.Collections; 25 | import java.util.Optional; 26 | 27 | /** 28 | * Default implementation of JwtCookiePrincipal 29 | */ 30 | public class DefaultJwtCookiePrincipal implements JwtCookiePrincipal { 31 | 32 | private final static String PERSISTENT = "pst"; // long-term token == rememberme 33 | private final static String ROLES = "rls"; 34 | 35 | protected final ClaimsBuilder claimsBuilder; 36 | 37 | /** 38 | * Builds a new instance of DefaultJwtCookiePrincipal 39 | * 40 | * @param name the principal name 41 | * @param persistent if the cookie must be persistent 42 | * @param roles the roles the principal is in 43 | * @param claims custom data associated with the principal 44 | */ 45 | public DefaultJwtCookiePrincipal( 46 | @JsonProperty("name") String name, 47 | @JsonProperty("persistent") boolean persistent, 48 | @JsonProperty("roles") Collection roles, 49 | @JsonProperty("claims") Claims claims) { 50 | this.claimsBuilder = Jwts.claims(); 51 | if (claims != null) { 52 | claimsBuilder.add(claims); 53 | } 54 | claimsBuilder.subject(name).add(PERSISTENT, persistent).add(ROLES, roles); 55 | } 56 | 57 | /** 58 | * Build a new instance of DefaultJwtCookiePrincipal with the given name 59 | * 60 | * @param name the name 61 | */ 62 | public DefaultJwtCookiePrincipal(String name) { 63 | this(name, false, Collections.emptyList(), null); 64 | } 65 | 66 | /** 67 | * Build a new instance of DefaultJwtCookiePrincipal from the JWT claims 68 | * 69 | * @param claims the JWT claims 70 | */ 71 | public DefaultJwtCookiePrincipal(Claims claims) { 72 | this.claimsBuilder = Jwts.claims(); 73 | if (claims != null) { 74 | claimsBuilder.add(claims); 75 | } 76 | } 77 | 78 | /** 79 | * Get the claims used to serialize this principal 80 | * 81 | * @return the claims 82 | */ 83 | public Claims getClaims() { 84 | return claimsBuilder.build(); 85 | } 86 | 87 | /** 88 | * Indicates if this principal has the given role 89 | * 90 | * @param role the role 91 | * @return true if the principal is in the given role, false otherwise 92 | */ 93 | @Override 94 | public boolean isInRole(String role) { 95 | return getRoles().contains(role); 96 | } 97 | 98 | /** 99 | * Get a collection of all the roles this principal is in 100 | * 101 | * @return the roles 102 | */ 103 | public Collection getRoles() { 104 | return Optional.ofNullable(getClaims().get(ROLES)) 105 | .map(Collection.class::cast) 106 | .orElse(Collections.emptyList()); 107 | } 108 | 109 | /** 110 | * Set the roles this principal is in 111 | * 112 | * @param roles the roles 113 | */ 114 | public void setRoles(Collection roles) { 115 | claimsBuilder.add(ROLES, roles); 116 | } 117 | 118 | /** 119 | * Indicates if the cookie must be persistent 120 | * 121 | * @return if the cookie must be persistent 122 | */ 123 | @Override 124 | public boolean isPersistent() { 125 | return getClaims().get(PERSISTENT) == Boolean.TRUE; 126 | } 127 | 128 | /** 129 | * Set if the cookie must be persistent 130 | * 131 | * @param persistent if the cookie must be persistent 132 | */ 133 | public void setPersistent(boolean persistent) { 134 | claimsBuilder.add(PERSISTENT, persistent); 135 | } 136 | 137 | /** 138 | * Get the name of the principal 139 | * 140 | * @return the name 141 | */ 142 | @Override 143 | public String getName() { 144 | return getClaims().getSubject(); 145 | } 146 | 147 | /** 148 | * Set the name of the principal 149 | * 150 | * @param name the name 151 | */ 152 | public void setName(String name) { 153 | claimsBuilder.subject(name); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/DontRefreshSession.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import jakarta.ws.rs.NameBinding; 19 | 20 | import java.lang.annotation.ElementType; 21 | import java.lang.annotation.Retention; 22 | import java.lang.annotation.RetentionPolicy; 23 | import java.lang.annotation.Target; 24 | 25 | /** 26 | * An annotation that can be used to avoid reseting the session TTL when an API is called 27 | */ 28 | @NameBinding 29 | @Retention(RetentionPolicy.RUNTIME) 30 | @Target({ElementType.METHOD, ElementType.TYPE}) 31 | public @interface DontRefreshSession { 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/DontRefreshSessionFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import jakarta.ws.rs.container.ContainerRequestContext; 19 | import jakarta.ws.rs.container.ContainerRequestFilter; 20 | 21 | import java.io.IOException; 22 | 23 | @DontRefreshSession 24 | public class DontRefreshSessionFilter implements ContainerRequestFilter { 25 | 26 | public static String DONT_REFRESH_SESSION_PROPERTY = "dontRefreshSession"; 27 | 28 | @Override 29 | public void filter(ContainerRequestContext requestContext) throws IOException { 30 | requestContext.setProperty(DONT_REFRESH_SESSION_PROPERTY, Boolean.TRUE); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthBundle.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import com.fasterxml.jackson.databind.module.SimpleModule; 19 | import com.google.common.hash.Hashing; 20 | import com.google.common.primitives.Ints; 21 | import io.dropwizard.auth.*; 22 | import io.dropwizard.core.Configuration; 23 | import io.dropwizard.core.ConfiguredBundle; 24 | import io.dropwizard.core.setup.Bootstrap; 25 | import io.dropwizard.core.setup.Environment; 26 | import io.dropwizard.jersey.setup.JerseyEnvironment; 27 | import io.jsonwebtoken.Claims; 28 | import io.jsonwebtoken.SignatureAlgorithm; 29 | import io.jsonwebtoken.impl.DefaultClaims; 30 | import jakarta.ws.rs.container.ContainerResponseFilter; 31 | import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; 32 | 33 | import javax.crypto.KeyGenerator; 34 | import javax.crypto.SecretKey; 35 | import javax.crypto.spec.SecretKeySpec; 36 | import java.nio.charset.StandardCharsets; 37 | import java.security.NoSuchAlgorithmException; 38 | import java.time.Duration; 39 | import java.util.Optional; 40 | import java.util.function.BiFunction; 41 | import java.util.function.Function; 42 | 43 | /** 44 | * Dopwizard bundle 45 | * 46 | * @param Your application configuration class 47 | * @param

the class of the principal that will be serialized in / deserialized from JWT cookies 48 | */ 49 | public class JwtCookieAuthBundle implements ConfiguredBundle { 50 | 51 | public static final String JWT_COOKIE_DEFAULT_NAME = "sessionToken"; 52 | private static final String JWT_COOKIE_PREFIX = "jwtCookie"; 53 | 54 | private final Class

principalType; 55 | private final Function serializer; 56 | private final Function deserializer; 57 | private Function configurationSupplier; 58 | private BiFunction keySuppplier; 59 | private UnauthorizedHandler unauthorizedHandler; 60 | 61 | /** 62 | * Get a bundle instance that will use DefaultJwtCookiePrincipal 63 | * 64 | * @param Your application configuration class 65 | * @return a bundle instance that will use DefaultJwtCookiePrincipal 66 | */ 67 | public static JwtCookieAuthBundle getDefault() { 68 | return new JwtCookieAuthBundle<>( 69 | DefaultJwtCookiePrincipal.class, 70 | DefaultJwtCookiePrincipal::getClaims, 71 | DefaultJwtCookiePrincipal::new); 72 | } 73 | 74 | /** 75 | * Build a new instance of JwtCookieAuthBundle 76 | * 77 | * @param principalType the class of the principal that will be serialized in / deserialized from JWT cookies 78 | * @param serializer a function to serialize principals into JWT claims 79 | * @param deserializer a function to deserialize JWT claims into principals 80 | */ 81 | public JwtCookieAuthBundle(Class

principalType, Function serializer, Function deserializer) { 82 | this.principalType = principalType; 83 | this.serializer = serializer; 84 | this.deserializer = deserializer; 85 | this.configurationSupplier = c -> new JwtCookieAuthConfiguration(); 86 | this.unauthorizedHandler = new DefaultUnauthorizedHandler(); 87 | } 88 | 89 | /** 90 | * If you want to sign the JWT with your own key, specify it here 91 | * 92 | * @param keySupplier a bi-function which will return the signing key from the configuration and environment 93 | * @return this 94 | */ 95 | public JwtCookieAuthBundle withKeyProvider(BiFunction keySupplier) { 96 | this.keySuppplier = keySupplier; 97 | return this; 98 | } 99 | 100 | /** 101 | * If you need to configure the bundle, specify it here 102 | * 103 | * @param configurationSupplier a bi-function which will return the bundle configuration from the application configuration 104 | * @return this 105 | */ 106 | public JwtCookieAuthBundle withConfigurationSupplier(Function configurationSupplier) { 107 | this.configurationSupplier = configurationSupplier; 108 | return this; 109 | } 110 | 111 | /** 112 | * If you want to use a different unauthorized handler, specify it here 113 | * 114 | * @param unauthorizedHandler an UnauthorizedHandler that will be used whenever a request fails to authenticate 115 | * @return this 116 | */ 117 | public JwtCookieAuthBundle withUnauthorizedHandler(UnauthorizedHandler unauthorizedHandler) { 118 | this.unauthorizedHandler = unauthorizedHandler; 119 | return this; 120 | } 121 | 122 | 123 | @Override 124 | public void initialize(Bootstrap bootstrap) { 125 | //in case somebody needs to serialize a DefaultJwtCookiePrincipal 126 | bootstrap.getObjectMapper().registerModule(new SimpleModule().addAbstractTypeMapping(Claims.class, DefaultClaims.class)); 127 | } 128 | 129 | @Override 130 | public void run(C configuration, Environment environment) throws Exception { 131 | JwtCookieAuthConfiguration conf = configurationSupplier.apply(configuration); 132 | 133 | //build the key from the key factory if it was provided 134 | SecretKey key = Optional 135 | .ofNullable(keySuppplier) 136 | .map(k -> k.apply(configuration, environment)) 137 | .orElseGet(() -> generateKey(conf.getSecretSeed())); 138 | 139 | JerseyEnvironment jerseyEnvironment = environment.jersey(); 140 | jerseyEnvironment.register(new AuthDynamicFeature(getAuthRequestFilter(key, conf.getCookieName()))); 141 | jerseyEnvironment.register(new AuthValueFactoryProvider.Binder<>(principalType)); 142 | jerseyEnvironment.register(RolesAllowedDynamicFeature.class); 143 | jerseyEnvironment.register(getAuthResponseFilter(key, conf)); 144 | jerseyEnvironment.register(DontRefreshSessionFilter.class); 145 | } 146 | 147 | /** 148 | * Get a filter that will deserialize the principal from JWT cookies found in HTTP requests 149 | * 150 | * @param key the key used to validate the JWT 151 | * @param cookieName the name of the cookie holding the JWT 152 | * @return the request filter 153 | */ 154 | public AuthFilter getAuthRequestFilter(SecretKey key, String cookieName) { 155 | return new JwtCookieAuthRequestFilter.Builder() 156 | .setCookieName(cookieName) 157 | .setAuthenticator(new JwtCookiePrincipalAuthenticator(key, deserializer)) 158 | .setPrefix(JWT_COOKIE_PREFIX) 159 | .setAuthorizer((Authorizer

) (principal, role, requestContext) -> principal.isInRole(role)) 160 | .setUnauthorizedHandler(unauthorizedHandler) 161 | .buildAuthFilter(); 162 | } 163 | 164 | /** 165 | * Get a filter that will deserialize the principal from JWT cookies found in HTTP requests, 166 | * using the default cookie name. 167 | * 168 | * @param key the key used to validate the JWT 169 | * @return the request filter 170 | */ 171 | public AuthFilter getAuthRequestFilter(SecretKey key) { 172 | return getAuthRequestFilter(key, JWT_COOKIE_DEFAULT_NAME); 173 | } 174 | 175 | /** 176 | * Get a filter that will serialize principals into JWTs and add them to HTTP response cookies 177 | * 178 | * @param key the key used to sign the JWT 179 | * @param configuration cookie configuration (secure, httpOnly, expiration...) 180 | * @return the response filter 181 | */ 182 | public ContainerResponseFilter getAuthResponseFilter(SecretKey key, JwtCookieAuthConfiguration configuration) { 183 | return new JwtCookieAuthResponseFilter<>( 184 | principalType, 185 | serializer, 186 | configuration.getCookieName(), 187 | configuration.isSecure(), 188 | configuration.isHttpOnly(), 189 | configuration.getDomain(), 190 | configuration.getSameSite(), 191 | key, 192 | Ints.checkedCast(Duration.parse(configuration.getSessionExpiryVolatile()).getSeconds()), 193 | Ints.checkedCast(Duration.parse(configuration.getSessionExpiryPersistent()).getSeconds())); 194 | } 195 | 196 | /** 197 | * Generate a HMAC SHA256 Key that can be used to sign JWTs 198 | * 199 | * @param secretSeed a seed from which the key will be generated. 200 | * Identical seeds will generate identical keys. 201 | * If null, a random key is returned. 202 | * @return a HMAC SHA256 Key 203 | */ 204 | public static SecretKey generateKey(String secretSeed) { 205 | // make a key from the seed if it was provided 206 | return Optional.ofNullable(secretSeed) 207 | .map(seed -> Hashing.sha256().newHasher().putString(seed, StandardCharsets.UTF_8).hash().asBytes()) 208 | .map(k -> (SecretKey) new SecretKeySpec(k, SignatureAlgorithm.HS256.getJcaName())) 209 | //else generate a random key 210 | .orElseGet(getHmacSha256KeyGenerator()::generateKey); 211 | } 212 | 213 | private static KeyGenerator getHmacSha256KeyGenerator() { 214 | try { 215 | return KeyGenerator.getInstance(SignatureAlgorithm.HS256.getJcaName()); 216 | } catch (NoSuchAlgorithmException e) { 217 | throw new SecurityException(e); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import jakarta.annotation.Nullable; 19 | import jakarta.validation.constraints.NotEmpty; 20 | 21 | import static org.dhatim.dropwizard.jwt.cookie.authentication.JwtCookieAuthBundle.JWT_COOKIE_DEFAULT_NAME; 22 | 23 | /** 24 | * Bundle configuration class 25 | */ 26 | public class JwtCookieAuthConfiguration { 27 | 28 | private String secretSeed; 29 | 30 | private String cookieName = JWT_COOKIE_DEFAULT_NAME; 31 | 32 | private boolean secure = false; 33 | 34 | private boolean httpOnly = true; 35 | 36 | @Nullable 37 | private SameSite sameSite = null; 38 | 39 | @Nullable 40 | private String domain = null; 41 | 42 | @NotEmpty 43 | private String sessionExpiryVolatile = "PT30m"; 44 | 45 | @NotEmpty 46 | private String sessionExpiryPersistent = "P7d"; 47 | 48 | /** 49 | * The secret seed use to generate the signing key. 50 | * It can be used to keep the same key value across application reboots. 51 | * 52 | * @return the signing key seed 53 | */ 54 | public String getSecretSeed() { 55 | return secretSeed; 56 | } 57 | 58 | /** 59 | * The name of the cookie holding the JWT. Its default value is "sessionToken". 60 | * 61 | * @return the cookie name 62 | */ 63 | public String getCookieName() { 64 | return cookieName; 65 | } 66 | 67 | /** 68 | * Check if the {@code Secure} cookie attribute is set, as described here. 69 | * 70 | * @return {@code true} if the {@code Secure} cookie attribute is set. 71 | */ 72 | public boolean isSecure() { 73 | return secure; 74 | } 75 | 76 | /** 77 | * Check if the {@code HttpOnly} cookie attribute is set, as described here. 78 | * 79 | * @return {@code true} if the {@code HttpOnly} cookie attribute is set. 80 | */ 81 | public boolean isHttpOnly() { 82 | return httpOnly; 83 | } 84 | 85 | /** 86 | * Duration of cookie, if volatile (in ISO 8601 format). 87 | * 88 | * @return the duration of a volatile cookie. 89 | */ 90 | public String getSessionExpiryVolatile() { 91 | return sessionExpiryVolatile; 92 | } 93 | 94 | /** 95 | * Duration of cookie, if persistent (in ISO 8601 format). 96 | * 97 | * @return the duration of a persistent cookie. 98 | */ 99 | public String getSessionExpiryPersistent() { 100 | return sessionExpiryPersistent; 101 | } 102 | 103 | /** 104 | * {@code SameSite} cookie attribute value, as described here. 105 | * 106 | * @return {@code SameSite} cookie attribute value, or {@code null} if not set 107 | */ 108 | public SameSite getSameSite() { 109 | return sameSite; 110 | } 111 | 112 | /** 113 | * {@code Domain} cookie attribute value, as described here. 114 | * 115 | * @return {@code Domain} cookie attribute value, or {@code null} if not set 116 | */ 117 | public String getDomain() { 118 | return domain; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthRequestFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import io.dropwizard.auth.AuthFilter; 19 | import io.dropwizard.auth.AuthenticationException; 20 | import jakarta.annotation.Priority; 21 | import jakarta.ws.rs.InternalServerErrorException; 22 | import jakarta.ws.rs.Priorities; 23 | import jakarta.ws.rs.container.ContainerRequestContext; 24 | import jakarta.ws.rs.core.Cookie; 25 | 26 | import java.io.IOException; 27 | import java.util.Objects; 28 | import java.util.Optional; 29 | 30 | @Priority(Priorities.AUTHENTICATION) 31 | class JwtCookieAuthRequestFilter

extends AuthFilter { 32 | 33 | private final String cookieName; 34 | 35 | private JwtCookieAuthRequestFilter(String cookieName) { 36 | this.cookieName = cookieName; 37 | } 38 | 39 | @Override 40 | public void filter(ContainerRequestContext crc) throws IOException { 41 | Cookie cookie = crc.getCookies().get(cookieName); 42 | if (null != cookie) { 43 | String accessToken = cookie.getValue(); 44 | if (accessToken != null && accessToken.length() > 0) { 45 | try { 46 | final Optional

subject = authenticator.authenticate(accessToken); 47 | if (subject.isPresent()) { 48 | CurrentPrincipal.set(subject.get()); 49 | crc.setSecurityContext(new JwtCookieSecurityContext(subject.get(), crc.getSecurityContext().isSecure())); 50 | return; 51 | } 52 | } catch (AuthenticationException e) { 53 | throw new InternalServerErrorException(e); 54 | } 55 | } 56 | } 57 | throw unauthorizedHandler.buildException(prefix, realm); 58 | } 59 | 60 | public static class Builder

extends AuthFilterBuilder> { 61 | 62 | private String cookieName; 63 | 64 | public Builder setCookieName(String cookieName) { 65 | this.cookieName = cookieName; 66 | return this; 67 | } 68 | 69 | @Override 70 | protected JwtCookieAuthRequestFilter

newInstance() { 71 | return new JwtCookieAuthRequestFilter(Objects.requireNonNull(cookieName, "cookieName is not set")); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthResponseFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import io.jsonwebtoken.Claims; 19 | import io.jsonwebtoken.Jwts; 20 | import io.jsonwebtoken.SignatureAlgorithm; 21 | import jakarta.ws.rs.container.ContainerRequestContext; 22 | import jakarta.ws.rs.container.ContainerResponseContext; 23 | import jakarta.ws.rs.container.ContainerResponseFilter; 24 | 25 | import java.io.IOException; 26 | import java.security.Key; 27 | import java.security.Principal; 28 | import java.time.Instant; 29 | import java.time.temporal.ChronoUnit; 30 | import java.util.Date; 31 | import java.util.function.Function; 32 | 33 | class JwtCookieAuthResponseFilter

implements ContainerResponseFilter { 34 | 35 | private static final String COOKIE_TEMPLATE = "=%s; Path=/"; 36 | private static final String SECURE_FLAG = "; Secure"; 37 | private static final String HTTP_ONLY_FLAG = "; HttpOnly"; 38 | private static final String DOMAIN_FLAG = "; Domain="; 39 | private static final String SAME_SITE_FLAG = "; SameSite="; 40 | private static final String DELETE_COOKIE_TEMPLATE = "=; Path=/; expires=Thu, 01-Jan-70 00:00:00 GMT"; 41 | 42 | private final Class

principalType; 43 | private final Function serializer; 44 | private final String cookieName; 45 | private final String sessionCookieFormat; 46 | private final String persistentCookieFormat; 47 | private final String deleteCookie; 48 | 49 | private final Key signingKey; 50 | private final int volatileSessionDuration; //in seconds 51 | private final int persistentSessionDuration; 52 | 53 | public JwtCookieAuthResponseFilter( 54 | Class

principalType, 55 | Function serializer, 56 | String cookieName, 57 | boolean secure, 58 | boolean httpOnly, 59 | String domain, 60 | SameSite sameSite, 61 | Key signingKey, 62 | int volatileSessionDuration, 63 | int persistentSessionDuration) { 64 | 65 | this.principalType = principalType; 66 | this.serializer = serializer; 67 | this.cookieName = cookieName; 68 | StringBuilder cookieFormatBuilder = new StringBuilder(cookieName).append(COOKIE_TEMPLATE); 69 | if (secure) { 70 | cookieFormatBuilder.append(SECURE_FLAG); 71 | } 72 | if (httpOnly) { 73 | cookieFormatBuilder.append(HTTP_ONLY_FLAG); 74 | } 75 | if (domain != null) { 76 | cookieFormatBuilder.append(DOMAIN_FLAG).append(domain); 77 | } 78 | if (sameSite != null) { 79 | cookieFormatBuilder.append(SAME_SITE_FLAG).append(sameSite.value); 80 | } 81 | this.sessionCookieFormat = cookieFormatBuilder.toString(); 82 | this.persistentCookieFormat = sessionCookieFormat + "; Max-Age=%d;"; 83 | StringBuilder deleteCookieBuilder = new StringBuilder(cookieName).append(DELETE_COOKIE_TEMPLATE); 84 | if (domain != null) { 85 | deleteCookieBuilder.append(DOMAIN_FLAG).append(domain); 86 | } 87 | this.deleteCookie = deleteCookieBuilder.toString(); 88 | this.signingKey = signingKey; 89 | this.volatileSessionDuration = volatileSessionDuration; 90 | this.persistentSessionDuration = persistentSessionDuration; 91 | } 92 | 93 | @Override 94 | public void filter(ContainerRequestContext request, ContainerResponseContext response) throws IOException { 95 | Principal principal = request.getSecurityContext().getUserPrincipal(); 96 | if (request.getSecurityContext() instanceof JwtCookieSecurityContext) { 97 | if (principalType.isInstance(principal)) { 98 | if (request.getProperty(DontRefreshSessionFilter.DONT_REFRESH_SESSION_PROPERTY) != Boolean.TRUE) { 99 | P cookiePrincipal = (P) principal; 100 | String cookie = cookiePrincipal.isPersistent() 101 | ? String.format(persistentCookieFormat, getJwt(cookiePrincipal, persistentSessionDuration), persistentSessionDuration) 102 | : String.format(sessionCookieFormat, getJwt(cookiePrincipal, volatileSessionDuration)); 103 | 104 | response.getHeaders().add("Set-Cookie", cookie); 105 | CurrentPrincipal.remove(); 106 | } 107 | } else if (request.getCookies().containsKey(cookieName)) { 108 | //the principal has been unset during the response, delete the cookie 109 | response.getHeaders().add("Set-Cookie", deleteCookie); 110 | } 111 | } 112 | } 113 | 114 | private String getJwt(P subject, int expiresIn) { 115 | return Jwts.builder() 116 | .signWith(signingKey, SignatureAlgorithm.HS256) 117 | .setClaims(serializer.apply(subject)) 118 | .setExpiration(Date.from(Instant.now().plus(expiresIn, ChronoUnit.SECONDS))) 119 | .compact(); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookiePrincipal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Dhatim. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import jakarta.ws.rs.container.ContainerRequestContext; 19 | 20 | import java.security.Principal; 21 | 22 | /** 23 | * A principal persisted in JWT cookies 24 | */ 25 | public interface JwtCookiePrincipal extends Principal { 26 | 27 | /** 28 | * Indicates if the cookie will be persistent (aka 'remember me') 29 | * 30 | * @return if the cookie must be persistent 31 | */ 32 | boolean isPersistent(); 33 | 34 | /** 35 | * Indicates if this principal has the given role 36 | * 37 | * @param role the role 38 | * @return true if the principal is in the given role, false otherwise 39 | */ 40 | boolean isInRole(String role); 41 | 42 | /** 43 | * Add this principal in the request context. 44 | * It will serialized in a JWT cookie and can be reused in subsequent queries 45 | * 46 | * @param context the request context 47 | */ 48 | default void addInContext(ContainerRequestContext context) { 49 | context.setSecurityContext(new JwtCookieSecurityContext(this, context.getSecurityContext().isSecure())); 50 | } 51 | 52 | public static void removeFromContext(ContainerRequestContext context) { 53 | context.setSecurityContext(new JwtCookieSecurityContext(null, context.getSecurityContext().isSecure())); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookiePrincipalAuthenticator.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import io.dropwizard.auth.AuthenticationException; 19 | import io.dropwizard.auth.Authenticator; 20 | import io.jsonwebtoken.Claims; 21 | import io.jsonwebtoken.ExpiredJwtException; 22 | import io.jsonwebtoken.Jwts; 23 | import io.jsonwebtoken.security.SecurityException; 24 | 25 | import javax.crypto.SecretKey; 26 | import java.util.Optional; 27 | import java.util.function.Function; 28 | 29 | class JwtCookiePrincipalAuthenticator

implements Authenticator { 30 | 31 | private final SecretKey key; 32 | private final Function deserializer; 33 | 34 | public JwtCookiePrincipalAuthenticator(SecretKey key, Function deserializer) { 35 | this.key = key; 36 | this.deserializer = deserializer; 37 | } 38 | 39 | @Override 40 | public Optional

authenticate(String credentials) throws AuthenticationException { 41 | try { 42 | return Optional.of(deserializer.apply(Jwts.parser().verifyWith(key).build().parseClaimsJws(credentials).getBody())); 43 | } catch (ExpiredJwtException | SecurityException e) { 44 | return Optional.empty(); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieSecurityContext.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import jakarta.ws.rs.core.SecurityContext; 19 | 20 | import java.security.Principal; 21 | import java.util.Optional; 22 | 23 | /** 24 | * Security context set after a JWT cookie authentication 25 | */ 26 | class JwtCookieSecurityContext implements SecurityContext { 27 | 28 | private final JwtCookiePrincipal subject; 29 | private final boolean secure; 30 | 31 | public JwtCookieSecurityContext(JwtCookiePrincipal subject, boolean secure) { 32 | this.subject = subject; 33 | this.secure = secure; 34 | } 35 | 36 | @Override 37 | public Principal getUserPrincipal() { 38 | return subject; 39 | } 40 | 41 | @Override 42 | public boolean isUserInRole(String role) { 43 | return Optional.ofNullable(subject) 44 | .map(s -> s.isInRole(role)) 45 | .orElse(false); 46 | } 47 | 48 | @Override 49 | public boolean isSecure() { 50 | return secure; 51 | } 52 | 53 | @Override 54 | public String getAuthenticationScheme() { 55 | return "JWT_COOKIE"; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/dhatim/dropwizard/jwt/cookie/authentication/SameSite.java: -------------------------------------------------------------------------------- 1 | package org.dhatim.dropwizard.jwt.cookie.authentication; 2 | 3 | public enum SameSite { 4 | 5 | NONE("None"), LAX("Lax"), STRICT("Strict"); 6 | 7 | public final String value; 8 | 9 | SameSite(String value) { 10 | this.value = value; 11 | } 12 | 13 | @Override 14 | public String toString() { 15 | return value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/org/dhatim/dropwizard/jwt/cookie/authentication/JwtCookieAuthenticationTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import io.dropwizard.client.HttpClientBuilder; 19 | import io.dropwizard.client.JerseyClientBuilder; 20 | import io.dropwizard.core.Configuration; 21 | import io.dropwizard.jackson.Jackson; 22 | import io.dropwizard.testing.junit5.DropwizardAppExtension; 23 | import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; 24 | import io.jsonwebtoken.lang.Strings; 25 | import jakarta.ws.rs.client.Client; 26 | import jakarta.ws.rs.client.Entity; 27 | import jakarta.ws.rs.client.WebTarget; 28 | import jakarta.ws.rs.core.MediaType; 29 | import jakarta.ws.rs.core.NewCookie; 30 | import jakarta.ws.rs.core.Response; 31 | import org.apache.hc.client5.http.cookie.BasicCookieStore; 32 | import org.junit.jupiter.api.Assertions; 33 | import org.junit.jupiter.api.BeforeAll; 34 | import org.junit.jupiter.api.Test; 35 | import org.junit.jupiter.api.extension.ExtendWith; 36 | 37 | import java.io.IOException; 38 | import java.io.InputStream; 39 | import java.io.InputStreamReader; 40 | import java.nio.charset.StandardCharsets; 41 | import java.time.Instant; 42 | import java.util.Collections; 43 | import java.util.Date; 44 | import java.util.UUID; 45 | 46 | @ExtendWith(DropwizardExtensionsSupport.class) 47 | public class JwtCookieAuthenticationTest { 48 | 49 | private static final DropwizardAppExtension EXT = new DropwizardAppExtension(TestApplication.class); 50 | 51 | private static Client CLIENT; 52 | 53 | @BeforeAll 54 | protected static void createClient() { 55 | JerseyClientBuilder builder = new JerseyClientBuilder(EXT.getEnvironment()); 56 | builder.using(Jackson.newObjectMapper()); 57 | builder.setApacheHttpClientBuilder(new HttpClientBuilder(EXT.getEnvironment()) { 58 | @Override 59 | protected org.apache.hc.client5.http.impl.classic.HttpClientBuilder customizeBuilder(org.apache.hc.client5.http.impl.classic.HttpClientBuilder builder) { 60 | return super.customizeBuilder(builder).setDefaultCookieStore(new BasicCookieStore()); 61 | } 62 | }); 63 | CLIENT = builder.build("client"); 64 | } 65 | 66 | private static final String COOKIE_NAME = "sessionToken"; 67 | 68 | private WebTarget getTarget() { 69 | return CLIENT.target("http://localhost:" + EXT.getLocalPort() + "/application/principal"); 70 | } 71 | 72 | @Test 73 | public void testUnauthorized() { 74 | //calls to APIs with the @Auth annotation without prior authentication should result in HTTP 401 75 | Response response = getTarget().request(MediaType.APPLICATION_JSON).get(); 76 | Assertions.assertEquals(401, response.getStatus()); 77 | } 78 | 79 | @Test 80 | public void testCookieSetting() throws IOException { 81 | String principalName = UUID.randomUUID().toString(); 82 | //a POST will set the principal 83 | Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(principalName))); 84 | Assertions.assertEquals(200, response.getStatus()); 85 | DefaultJwtCookiePrincipal principal = getPrincipal(response); 86 | Assertions.assertEquals(principalName, principal.getName()); 87 | 88 | //check that a session cookie has been set 89 | NewCookie cookie1 = response.getCookies().get(COOKIE_NAME); 90 | Assertions.assertNotNull(cookie1); 91 | Assertions.assertTrue(Strings.hasText(cookie1.getValue())); 92 | Assertions.assertTrue(cookie1.isHttpOnly()); 93 | 94 | //a GET with this cookie should return the Principal and refresh the cookie 95 | response = getTarget().request(MediaType.APPLICATION_JSON).cookie(cookie1).get(); 96 | Assertions.assertEquals(200, response.getStatus()); 97 | principal = getPrincipal(response); 98 | Assertions.assertEquals(principalName, principal.getName()); 99 | NewCookie cookie2 = response.getCookies().get(COOKIE_NAME); 100 | Assertions.assertNotNull(cookie2); 101 | Assertions.assertTrue(Strings.hasText(cookie1.getValue())); 102 | Assertions.assertNotSame(cookie1.getValue(), cookie2.getValue()); 103 | } 104 | 105 | @Test 106 | public void testDontRefreshSession() throws IOException { 107 | //requests made to methods annotated with @DontRefreshSession should not modify the cookie 108 | String principalName = UUID.randomUUID().toString(); 109 | Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(principalName))); 110 | NewCookie cookie = response.getCookies().get(COOKIE_NAME); 111 | 112 | response = getTarget().path("idempotent").request(MediaType.APPLICATION_JSON).cookie(cookie).get(); 113 | Assertions.assertEquals(200, response.getStatus()); 114 | Assertions.assertEquals(principalName, getPrincipal(response).getName()); 115 | Assertions.assertNull(response.getCookies().get(COOKIE_NAME)); 116 | } 117 | 118 | @Test 119 | public void testPublicEndpoint() { 120 | //public endpoints (i.e. not with @Auth, @RolesAllowed etc.) should not modify the cookie 121 | Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(UUID.randomUUID().toString()))); 122 | NewCookie cookie = response.getCookies().get(COOKIE_NAME); 123 | 124 | //request made to public methods should not refresh the cookie 125 | response = getTarget().path("public").request(MediaType.APPLICATION_JSON).cookie(cookie).get(); 126 | Assertions.assertEquals(200, response.getStatus()); 127 | Assertions.assertNull(response.getCookies().get(COOKIE_NAME)); 128 | } 129 | 130 | @Test 131 | public void testRememberMe() { 132 | //a volatile principal should set a volatile cookie 133 | DefaultJwtCookiePrincipal principal = new DefaultJwtCookiePrincipal(UUID.randomUUID().toString()); 134 | Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(principal)); 135 | NewCookie cookie = response.getCookies().get(COOKIE_NAME); 136 | Assertions.assertNotNull(cookie); 137 | Assertions.assertEquals(-1, cookie.getMaxAge()); 138 | 139 | //a long term principal should set a persistent cookie 140 | principal.setPersistent(true); 141 | response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(principal)); 142 | cookie = response.getCookies().get(COOKIE_NAME); 143 | //default maxAge is 604800s (7 days) 144 | Assertions.assertNotNull(cookie); 145 | Assertions.assertEquals(604800, cookie.getMaxAge()); 146 | } 147 | 148 | @Test 149 | public void testRoles() { 150 | WebTarget restrictedTarget = getTarget().path("restricted"); 151 | //try to access the resource without cookie (-> 401 UNAUTHORIZED) 152 | Response response = restrictedTarget.request().get(); 153 | Assertions.assertEquals(401, response.getStatus()); 154 | 155 | //set a principal without the admin role (-> 403 FORBIDDEN) 156 | DefaultJwtCookiePrincipal principal = new DefaultJwtCookiePrincipal(UUID.randomUUID().toString()); 157 | response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(principal)); 158 | NewCookie cookie = response.getCookies().get(COOKIE_NAME); 159 | Assertions.assertNotNull(cookie); 160 | response = restrictedTarget.request().cookie(cookie).get(); 161 | Assertions.assertEquals(403, response.getStatus()); 162 | 163 | //set a principal with the admin role (-> 200 OK) 164 | principal.setRoles(Collections.singleton("admin")); 165 | response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(principal)); 166 | cookie = response.getCookies().get(COOKIE_NAME); 167 | Assertions.assertNotNull(cookie); 168 | response = restrictedTarget.request().cookie(cookie).get(); 169 | Assertions.assertEquals(200, response.getStatus()); 170 | } 171 | 172 | @Test 173 | public void testDeleteCookie() { 174 | Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(UUID.randomUUID().toString()))); 175 | NewCookie cookie = response.getCookies().get(COOKIE_NAME); 176 | Assertions.assertNotNull(cookie); 177 | 178 | //removing the principal should produce a cookie with empty contenant and a past expiration date 179 | response = getTarget().path("unset").request().cookie(cookie).get(); 180 | Assertions.assertEquals(204, response.getStatus()); 181 | cookie = response.getCookies().get(COOKIE_NAME); 182 | Assertions.assertNotNull(cookie); 183 | Assertions.assertEquals("", cookie.getValue()); 184 | Assertions.assertEquals(Date.from(Instant.EPOCH), cookie.getExpiry()); 185 | } 186 | 187 | @Test 188 | public void testGetCurrentPrincipal() throws IOException { 189 | //test to get principal from CurrentPrincipal.get() instead of @Auth 190 | String principalName = UUID.randomUUID().toString(); 191 | Response response = getTarget().request(MediaType.APPLICATION_JSON).post(Entity.json(new DefaultJwtCookiePrincipal(principalName))); 192 | NewCookie cookie = response.getCookies().get(COOKIE_NAME); 193 | Assertions.assertNotNull(cookie); 194 | 195 | response = getTarget().path("current").request(MediaType.APPLICATION_JSON).cookie(cookie).get(); 196 | Assertions.assertEquals(200, response.getStatus()); 197 | Assertions.assertEquals(principalName, getPrincipal(response).getName()); 198 | } 199 | 200 | private DefaultJwtCookiePrincipal getPrincipal(Response response) throws IOException { 201 | return EXT.getObjectMapper() 202 | .reader() 203 | .forType(DefaultJwtCookiePrincipal.class) 204 | .readValue(new InputStreamReader((InputStream) response.getEntity(), StandardCharsets.UTF_8)); 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /src/test/java/org/dhatim/dropwizard/jwt/cookie/authentication/TestApplication.java: -------------------------------------------------------------------------------- 1 | package org.dhatim.dropwizard.jwt.cookie.authentication; 2 | 3 | import com.codahale.metrics.health.HealthCheck; 4 | import io.dropwizard.core.Application; 5 | import io.dropwizard.core.Configuration; 6 | import io.dropwizard.core.server.SimpleServerFactory; 7 | import io.dropwizard.core.setup.Bootstrap; 8 | import io.dropwizard.core.setup.Environment; 9 | import io.dropwizard.jetty.HttpConnectorFactory; 10 | import io.dropwizard.logging.common.DefaultLoggingFactory; 11 | 12 | public class TestApplication extends Application { 13 | 14 | @Override 15 | public void initialize(Bootstrap bootstrap) { 16 | bootstrap.addBundle(JwtCookieAuthBundle.getDefault()); 17 | } 18 | 19 | @Override 20 | public void run(Configuration configuration, Environment environment) { 21 | ((DefaultLoggingFactory)configuration.getLoggingFactory()).setLevel("DEBUG"); 22 | 23 | //choose a random port 24 | SimpleServerFactory serverConfig = new SimpleServerFactory(); 25 | configuration.setServerFactory(serverConfig); 26 | HttpConnectorFactory connectorConfig = (HttpConnectorFactory) serverConfig.getConnector(); 27 | connectorConfig.setPort(0); 28 | 29 | //Dummy health check to suppress the startup warning. 30 | environment.healthChecks().register("dummy", new HealthCheck() { 31 | @Override 32 | protected HealthCheck.Result check() { 33 | return HealthCheck.Result.healthy(); 34 | } 35 | }); 36 | 37 | environment.jersey().register(new TestResource()); 38 | System.out.println("*** started"); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/test/java/org/dhatim/dropwizard/jwt/cookie/authentication/TestResource.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 Dhatim 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | package org.dhatim.dropwizard.jwt.cookie.authentication; 17 | 18 | import io.dropwizard.auth.Auth; 19 | import jakarta.annotation.security.RolesAllowed; 20 | import jakarta.ws.rs.*; 21 | import jakarta.ws.rs.container.ContainerRequestContext; 22 | import jakarta.ws.rs.core.Context; 23 | import jakarta.ws.rs.core.MediaType; 24 | 25 | @Path("principal") 26 | public class TestResource { 27 | 28 | @POST 29 | @Consumes(MediaType.APPLICATION_JSON) 30 | @Produces(MediaType.APPLICATION_JSON) 31 | public DefaultJwtCookiePrincipal setPrincipal(@Context ContainerRequestContext requestContext, DefaultJwtCookiePrincipal principal){ 32 | principal.addInContext(requestContext); 33 | return principal; 34 | } 35 | 36 | @GET 37 | @Path("unset") 38 | public void unsetPrincipal(@Context ContainerRequestContext context){ 39 | JwtCookiePrincipal.removeFromContext(context); 40 | } 41 | 42 | @GET 43 | @Produces(MediaType.APPLICATION_JSON) 44 | public DefaultJwtCookiePrincipal getPrincipal(@Auth DefaultJwtCookiePrincipal principal){ 45 | return principal; 46 | } 47 | 48 | @GET 49 | @Path("idempotent") 50 | @Produces(MediaType.APPLICATION_JSON) 51 | @DontRefreshSession 52 | public DefaultJwtCookiePrincipal getPrincipalWithoutRefreshingSession(@Auth DefaultJwtCookiePrincipal principal){ 53 | return principal; 54 | } 55 | 56 | @GET 57 | @Path("restricted") 58 | @RolesAllowed("admin") 59 | public String getRestrictedResource(){ 60 | return "SuperSecretStuff"; 61 | } 62 | 63 | @GET 64 | @Path("public") 65 | public String getPublicResource(){ 66 | return "PublicStuff"; 67 | } 68 | 69 | @GET 70 | @Path("current") 71 | @Produces(MediaType.APPLICATION_JSON) 72 | public DefaultJwtCookiePrincipal getCurrentPrincipal(@Auth DefaultJwtCookiePrincipal principal){ 73 | return CurrentPrincipal.get(); 74 | } 75 | } 76 | --------------------------------------------------------------------------------