├── .github └── dependabot.yml ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── RELEASE.md ├── pom.xml ├── src ├── main │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── majusko │ │ │ └── grpc │ │ │ └── jwt │ │ │ ├── GrpcJwtAutoConfiguration.java │ │ │ ├── GrpcJwtProperties.java │ │ │ ├── GrpcJwtSpringBootStarterApplication.java │ │ │ ├── annotation │ │ │ ├── Allow.java │ │ │ └── Exposed.java │ │ │ ├── data │ │ │ ├── AllowedMethod.java │ │ │ ├── GrpcHeader.java │ │ │ ├── GrpcJwtContext.java │ │ │ └── JwtContextData.java │ │ │ ├── exception │ │ │ ├── AuthException.java │ │ │ └── UnauthenticatedException.java │ │ │ ├── interceptor │ │ │ ├── AllowedCollector.java │ │ │ ├── AuthClientInterceptor.java │ │ │ └── AuthServerInterceptor.java │ │ │ └── service │ │ │ ├── GrpcRole.java │ │ │ ├── JwtService.java │ │ │ └── dto │ │ │ ├── JwtData.java │ │ │ ├── JwtMetadata.java │ │ │ └── JwtToken.java │ └── resources │ │ ├── META-INF │ │ └── spring.factories │ │ └── application.properties └── test │ ├── java │ └── io │ │ └── github │ │ └── majusko │ │ └── grpc │ │ └── jwt │ │ └── GrpcJwtSpringBootStarterApplicationTest.java │ └── proto │ └── Example.proto └── travis-settings.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: io.grpc:grpc-testing 10 | versions: 11 | - 1.35.0 12 | - 1.36.0 13 | - 1.36.1 14 | - dependency-name: org.springframework.boot:spring-boot-starter-parent 15 | versions: 16 | - 2.4.3 17 | - dependency-name: io.github.lognet:grpc-spring-boot-starter 18 | versions: 19 | - 4.4.3 20 | - 4.4.4 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | 33 | my-settings.xml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | dist: trusty 5 | after_success: 6 | - bash <(curl -s https://codecov.io/bash) 7 | - "[[ $TRAVIS_BRANCH == \"master\" ]] && { mvn deploy --settings travis-settings.xml -DskipTests=true -B; };" 8 | before_deploy: 9 | - "mvn -DskipTests package" 10 | - export FILE_TO_UPLOAD=$(ls target/grpc-jwt-spring-boot-starter-*.jar) 11 | - "echo $FILE_TO_UPLOAD" 12 | deploy: 13 | provider: releases 14 | api_key: 15 | secure: Mef8otNBMl1IAdGyyp78UFJO6O++5HIWu8Q9gzd3a02HpV2Q8bEbgXSTXMB15ZyD4TMHv+B7sw+94XfJyUkqAHoyNJJcJIFL4v0L9wVdKgJm+gnOWlFR4ZFybOLqSpvpoQFj8W9Cdz2eF+P0LUyVcz0qLhYbe1m/IM+OVaaQMBBTZea7xIiHp+Qd2h5+eenqaYBnsrAMHGtv92KTswr3kw3wU2k4Dvh6Bd1GZVW75R+NTP7LMRuaEbVfgnYE07EsBqjBDoSXWQ4w+S/SPEJyDtHESBNd57RyW+zZaLDd5kWrhH+dI5My4m20znZoMIhbEiqdbQgy7EU5YFThDFgmx4qMqVEUI9cKBJAtsE4pTsOtNNjeBeQ+/AH/OQG4Nfp9yJVVgKjUnR9QORpdAFwKbbK4Xx5hVG0VVB2xDKqw5HH1MTVc9KkCtnOO5RQlJ9BCeHB9jXtvD4cdGyZAib9HbSEadpzd8MJFnpFlaErOuWvs+zOOPgr8pmNDnu/NP82V+hbgbRpBHxotgatVy+G6pEMLD4rQ8HykIHZEcYb3BcASZ4PUkig7yW+7DPzFwnmSJLlgE5H4C9mYQmy2vhyl5IXgHN4Gqnlwqt46KKuUiExI1qEjS0eUNYW7g0unhiELAvUDnwwt4d2N03d9R1eCYCWJgdUgOAIfpeRKW3GkR+4= 16 | file: 17 | - "${FILE_TO_UPLOAD}" 18 | skip_cleanup: true 19 | overwrite: true 20 | on: 21 | repo: majusko/grpc-jwt-spring-boot-starter 22 | tags: true 23 | branch: master 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | MIT License 4 | 5 | Copyright (c) 2019 Mario Kapusta 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring boot starter for [gRPC framework](https://grpc.io/) with [JWT authorization](https://jwt.io/) - gRPC Java JWT 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.github.majusko/grpc-jwt-spring-boot-starter/badge.svg)](https://search.maven.org/search?q=g:io.github.majusko) 4 | [![Release](https://jitpack.io/v/majusko/grpc-jwt-spring-boot-starter.svg)](https://jitpack.io/#majusko/grpc-jwt-spring-boot-starter) 5 | [![Build Status](https://travis-ci.com/majusko/grpc-jwt-spring-boot-starter.svg?branch=master)](https://travis-ci.com/majusko/grpc-jwt-spring-boot-starter) 6 | [![Test Coverage](https://codecov.io/gh/majusko/grpc-jwt-spring-boot-starter/branch/master/graph/badge.svg)](https://codecov.io/gh/majusko/grpc-jwt-spring-boot-starter/branch/master) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Join the chat at https://gitter.im/grpc-jwt-spring-boot-starter/community](https://badges.gitter.im/grpc-jwt-spring-boot-starter/community.svg)](https://gitter.im/grpc-jwt-spring-boot-starter/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | Extending great [gRPC library](https://github.com/LogNet/grpc-spring-boot-starter) with Auth module. Easy implementation using a simple annotations similar to ones used in Spring Security module. 10 | 11 | ## Quick Start 12 | (Try example project: [gRPC example project](https://github.com/majusko/grpc-example) in Kotlin.) 13 | 14 | Simple start consist only from 3 simple steps. 15 | 16 | (If you never used [gRPC library](https://github.com/LogNet/grpc-spring-boot-starter) before, have a look on this [basic setup](https://github.com/LogNet/grpc-spring-boot-starter#4-show-case) first.) 17 | 18 | #### 1. Add Maven dependency 19 | 20 | ```xml 21 | 22 | io.github.majusko 23 | grpc-jwt-spring-boot-starter 24 | ${version} 25 | 26 | ``` 27 | 28 | #### 2. Add `@Allow` annotation to your service method 29 | 30 | All you need to do is to annotate your method in the service implementation. 31 | 32 | ```java 33 | @GRpcService 34 | public class ExampleServiceImpl extends ExampleServiceGrpc.ExampleServiceImplBase { 35 | 36 | @Allow(roles = GrpcRole.INTERNAL) 37 | public void getExample(GetExample request, StreamObserver response) { 38 | //... 39 | } 40 | } 41 | ``` 42 | 43 | #### 3. Add interceptor to client 44 | 45 | Just autowire already prepared `AuthClientInterceptor` bean and intercept your client. It will inject the internal token to every request by default. 46 | 47 | ```java 48 | @Service 49 | public class ExampleClient { 50 | 51 | @Autowired 52 | private AuthClientInterceptor authClientInterceptor; 53 | 54 | public void exampleRequest() { 55 | final ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); 56 | final Channel interceptedChannel = ClientInterceptors.intercept(channel,authClientInterceptor); 57 | final ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(interceptedChannel); 58 | 59 | stub.getExample(GetExample.newBuilder().build()); 60 | } 61 | } 62 | ``` 63 | 64 | ## Documentation 65 | 66 | ### 0. Basic setup of gRPC 67 | Useful only in case you never heard about [gRPC library from LogNet](https://github.com/LogNet/grpc-spring-boot-starter). 68 | You can find there a nice [show case](https://github.com/LogNet/grpc-spring-boot-starter#4-show-case) too. 69 | 70 | #### 0.1 Service implementation 71 | 72 | The service definition from .proto file looks like this 73 | 74 | ```proto 75 | service ExampleService { 76 | rpc GetExample (GetExample) returns (Empty) {}; 77 | } 78 | 79 | message Empty {} 80 | message GetExample { 81 | string ownerField = 1; 82 | } 83 | ``` 84 | 85 | #### 0.2 Service implementation 86 | 87 | All you need to do is to annotate your service implementation with `GRpcService` 88 | 89 | ```java 90 | @GRpcService 91 | public class ExampleServiceImpl extends ExampleServiceGrpc.ExampleServiceImplBase { 92 | 93 | public void getExample(GetExample request, StreamObserver response) { 94 | response.onNext(Empty.newBuilder().build()); 95 | response.onCompleted(); 96 | } 97 | } 98 | ``` 99 | 100 | ### 1. Configuration 101 | 102 | You can use `application.properties` to override the default configuration. 103 | 104 | * `grpc.jwt.algorithm` -> Algorithm used for signing the JWT token. Default: `HmacSHA256` 105 | * `grpc.jwt.secret` -> String used as a secret to sign the JWT token. Default: `default` 106 | * `grpc.jwt.expirationSec` -> Number of seconds needed to token becoming expired. Default: `3600` 107 | 108 | ``` 109 | grpc.jwt.algorithm=HmacSHA256 110 | grpc.jwt.secret=secret 111 | grpc.jwt.expirationSec=3600 112 | ``` 113 | 114 | ### 2. Annotations 115 | 116 | We know 2 types of annotation: `@Allow` and `@Expose` 117 | 118 | #### `@Allow` 119 | * `roles` -> Algorithm used for signing the JWT token. Default: `HmacSHA256` 120 | * `ownerField` -> Example: `ownerField`. _Optional field_. Your request will be parsed and if the mentioned field is found, it will compare equality with JWT token subject(e.g.: ownerField). By this comparison, you can be sure that any operation with that field is made by the owner of the token. If the fields don't match and data are owned by another user, specified roles will be checked after. 121 | 122 | 123 | _**Example use case of `ownerField`**: Imagine, you want to list purchased orders of some user. 124 | You might want to reuse the exact same API for back-office and also for that particular user who created the orders. 125 | With `ownerField` you can check for the owner and also for some role if owner ownerField in JWT token is different._ 126 | 127 | #### `@Exposed` 128 | * `environments` List of environments (Spring Profiles) where you can access the gRPC without checking for owner or roles. 129 | Use case: Debug endpoint for the client/front-end development team. 130 | 131 | ```java 132 | @GRpcService 133 | public class ExampleServiceImpl extends ExampleServiceGrpc.ExampleServiceImplBase { 134 | 135 | @Allow(ownerField="ownerField", roles = GrpcRole.INTERNAL) 136 | @Exposed(environments={"dev","qa"}) 137 | public void getExample(GetExample request, StreamObserver response) { 138 | //... 139 | } 140 | } 141 | ``` 142 | 143 | ### Token generation 144 | 145 | You will need to generate tokens for your users or clients. You might want to specify special roles for each user and also service method. You can use the `JwtService` for simple and performing usage. 146 | 147 | ```java 148 | @Service 149 | public class SomeClass { 150 | 151 | private final static String ADMIN = "admin"; 152 | 153 | @Autowired 154 | private JwtService jwtService; 155 | 156 | public void someMethod() { 157 | final JwtData data = new JwtData("user-id-12345", new HashSet<>(ADMIN)); 158 | 159 | final String token = jwtService.generate(data); 160 | } 161 | } 162 | ``` 163 | 164 | ### Making requests 165 | 166 | We have two types of usages for client. 167 | 168 | 1. Inter-service communication. 169 | 2. User communication. 170 | 171 | #### 1. Client for inter-service communication only. 172 | 173 | * Autowire `AuthClientInterceptor` to your service. 174 | * Register your interceptor with native `ClientInterceptors` class. 175 | * Call request from gRPC generated stub. 176 | 177 | ```java 178 | @Service 179 | public class SomeClass { 180 | 181 | @Autowired 182 | private AuthClientInterceptor authClientInterceptor; 183 | 184 | public void customTokenRequest() { 185 | 186 | final ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); 187 | final Channel interceptedChannel = ClientInterceptors.intercept(channel,authClientInterceptor); 188 | final ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(interceptedChannel); 189 | 190 | final Empty response = stub.getExample(GetExample.newBuilder().setUserId("user-id-jr834fh").build()); 191 | } 192 | } 193 | ``` 194 | 195 | #### 2. Client for custom token communication. 196 | 197 | * Add your token generated with `JwtService` to gRPC header with `GrpcHeader.AUTHORIZATION` 198 | * Call request from gRPC generated stub. 199 | 200 | ```java 201 | @Service 202 | public class SomeClass { 203 | 204 | public void customTokenRequest() { 205 | 206 | final Metadata header = new Metadata(); 207 | header.put(GrpcHeader.AUTHORIZATION, "jwt-token-r348hf34hf43f93"); 208 | 209 | final ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); 210 | final ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 211 | final ExampleServiceBlockingStub stubWithHeaders = MetadataUtils.attachHeaders(stub, header); 212 | 213 | final Empty response = stub.getExample(GetExample.newBuilder().setUserId("user-id-jr834fh").build()); 214 | } 215 | } 216 | ``` 217 | 218 | ### Tests 219 | 220 | The library is fully covered with integration tests which are also very useful as a usage example. 221 | 222 | `GrpcJwtSpringBootStarterApplicationTest` 223 | 224 | ## Contributing 225 | 226 | All contributors are welcome. If you never contributed to the open-source, start with reading the [Github Flow](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/github-flow). 227 | 228 | 1. Create an [issue](https://help.github.com/en/github/managing-your-work-on-github/about-issues) 229 | 2. Create a [pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) with reference to the issue 230 | 3. Rest and enjoy the great feeling of being a contributor. 231 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | ### 1. On some machines you need to run this first 4 | 5 | Because of: https://github.com/keybase/keybase-issues/issues/2798 6 | 7 | ```yaml 8 | export GPG_TTY=$(tty) 9 | ``` 10 | 11 | ### 2. Prepare your `my-settings.xml` 12 | 13 | ```xml 14 | 15 | 16 | ossrh 17 | ${env.SONATYPE_USERNAME} 18 | ${env.SONATYPE_PASSWORD} 19 | 20 | 21 | ``` 22 | 23 | ### 3. Execute release command 24 | 25 | ```yaml 26 | mvn clean deploy -P release-sign-artifacts --settings my-settings.xml 27 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.5.4 9 | 10 | 11 | 12 | io.github.majusko 13 | grpc-jwt-spring-boot-starter 14 | 1.0.10 15 | grpc-jwt-spring-boot-starter 16 | JWT Auth for your Grpc Spring Boot project 17 | 18 | 19 | 1.8 20 | 1.8 21 | 1.8 22 | 1.41.0 23 | 1.7.0 24 | 0.6.1 25 | 26 | 27 | 28 | 29 | 30 | commons-codec 31 | commons-codec 32 | 1.15 33 | 34 | 35 | 36 | io.jsonwebtoken 37 | jjwt-api 38 | 0.11.2 39 | 40 | 41 | 42 | io.jsonwebtoken 43 | jjwt-impl 44 | 0.11.2 45 | runtime 46 | 47 | 48 | 49 | io.jsonwebtoken 50 | jjwt-jackson 51 | 0.11.2 52 | runtime 53 | 54 | 55 | 56 | io.github.lognet 57 | grpc-spring-boot-starter 58 | 4.5.8 59 | 60 | 61 | 62 | org.projectlombok 63 | lombok 64 | 1.18.20 65 | provided 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-starter-test 73 | test 74 | 75 | 76 | 77 | junit 78 | junit 79 | 80 | 81 | org.junit.vintage 82 | junit-vintage-engine 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.junit.jupiter 90 | junit-jupiter-engine 91 | ${junit-jupiter.version} 92 | test 93 | 94 | 95 | 96 | io.grpc 97 | grpc-testing 98 | ${grpc.version} 99 | test 100 | 101 | 102 | 103 | 104 | 105 | 106 | release-sign-artifacts 107 | 108 | 109 | performRelease 110 | true 111 | 112 | 113 | 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-source-plugin 118 | 3.2.1 119 | 120 | 121 | attach-sources 122 | 123 | jar-no-fork 124 | 125 | 126 | 127 | 128 | 129 | org.apache.maven.plugins 130 | maven-javadoc-plugin 131 | 3.3.1 132 | 133 | false 134 | 135 | 136 | 137 | attach-javadocs 138 | 139 | jar 140 | 141 | 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-gpg-plugin 147 | 148 | 149 | sign-artifacts 150 | verify 151 | 152 | sign 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | kr.motd.maven 167 | os-maven-plugin 168 | ${os-maven-plugin.version} 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | org.xolstice.maven.plugins 178 | protobuf-maven-plugin 179 | ${protobuf-maven-plugin.version} 180 | 181 | 182 | com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier} 183 | 184 | grpc-java 185 | 186 | io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier} 187 | 188 | ${basedir}/src/test/proto 189 | 190 | 191 | 192 | 193 | compile 194 | compile-custom 195 | 196 | 197 | 198 | 199 | 200 | 201 | org.springframework.boot 202 | spring-boot-maven-plugin 203 | 204 | 205 | 206 | repackage 207 | 208 | repackage 209 | 210 | 211 | false 212 | 213 | 214 | 215 | 216 | 217 | 218 | maven-compiler-plugin 219 | 3.8.1 220 | 221 | ${maven.compiler.source} 222 | ${maven.compiler.target} 223 | 224 | 225 | 226 | 227 | org.jacoco 228 | jacoco-maven-plugin 229 | 0.8.7 230 | 231 | 232 | **/*Application.* 233 | **/*Builder* 234 | **/*Properties.* 235 | **/*Configuration.* 236 | **/proto/**/* 237 | 238 | 239 | 240 | 241 | 242 | prepare-agent 243 | 244 | 245 | 246 | report 247 | test 248 | 249 | report 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | ossrh 260 | https://oss.sonatype.org/content/repositories/snapshots 261 | 262 | 263 | ossrh 264 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 265 | 266 | 267 | 268 | 269 | 270 | Apache License, Version 2.0 271 | http://www.apache.org/licenses/LICENSE-2.0.txt 272 | repo 273 | 274 | 275 | 276 | 277 | scm:git:https://github.com/majusko/grpc-jwt-spring-boot-starter.git 278 | https://github.com/majusko/grpc-jwt-spring-boot-starter 279 | 280 | 281 | 282 | 283 | Mario Kapusta 284 | mariokapustaa@gmail.com 285 | https://github.com/majusko 286 | 287 | 288 | 289 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/GrpcJwtAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt; 2 | 3 | import io.github.majusko.grpc.jwt.interceptor.AuthClientInterceptor; 4 | import io.github.majusko.grpc.jwt.service.JwtService; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.core.env.Environment; 10 | 11 | @Configuration 12 | @ComponentScan 13 | @EnableConfigurationProperties(GrpcJwtProperties.class) 14 | public class GrpcJwtAutoConfiguration { 15 | 16 | private final Environment environment; 17 | private final GrpcJwtProperties grpcJwtProperties; 18 | 19 | public GrpcJwtAutoConfiguration(Environment environment, GrpcJwtProperties grpcJwtProperties) { 20 | this.environment = environment; 21 | this.grpcJwtProperties = grpcJwtProperties; 22 | } 23 | 24 | @Bean 25 | public JwtService jwtService() { 26 | return new JwtService(environment, grpcJwtProperties); 27 | } 28 | 29 | @Bean 30 | public AuthClientInterceptor authClientInterceptor() { 31 | return new AuthClientInterceptor(jwtService()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/GrpcJwtProperties.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties(prefix = "grpc.jwt") 8 | public class GrpcJwtProperties { 9 | private String secret = "default"; 10 | private String algorithm = "HmacSHA256"; 11 | private Long expirationSec = 3600L; 12 | } -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/GrpcJwtSpringBootStarterApplication.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class GrpcJwtSpringBootStarterApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(GrpcJwtSpringBootStarterApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/annotation/Allow.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.annotation; 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 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface Allow { 11 | 12 | /** 13 | * List of roles that will by checked. One of the roles must be presented in JWT token. 14 | */ 15 | String[] roles() default {}; 16 | 17 | /** 18 | * Optional field. Ownership of entity will be checked first by getting owners id from payload by 19 | * field specified in annotation. If the id does not match and data are owned by other authority, 20 | * specified roles will be checked then. 21 | */ 22 | String ownerField() default ""; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/annotation/Exposed.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.annotation; 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 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface Exposed { 11 | 12 | /** 13 | * List of environments where you can access the endpoint without role or owner authorization. 14 | */ 15 | String[] environments() default {}; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/data/AllowedMethod.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.data; 2 | 3 | import java.util.Objects; 4 | import java.util.Set; 5 | 6 | public class AllowedMethod { 7 | private final String method; 8 | private final String ownerField; 9 | private final Set roles; 10 | 11 | public AllowedMethod(String method, String ownerField, Set roles) { 12 | this.method = Objects.requireNonNull(method); 13 | this.ownerField = Objects.requireNonNull(ownerField); 14 | this.roles = Objects.requireNonNull(roles); 15 | } 16 | 17 | public String getMethod() { 18 | return method; 19 | } 20 | 21 | public String getOwnerField() { 22 | return ownerField; 23 | } 24 | 25 | public Set getRoles() { 26 | return roles; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/data/GrpcHeader.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.data; 2 | 3 | import io.grpc.Metadata; 4 | 5 | import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; 6 | 7 | public class GrpcHeader { 8 | private GrpcHeader() { 9 | } 10 | 11 | private static final String AUTHORIZATION_KEY = "Authorization"; 12 | 13 | public static final Metadata.Key AUTHORIZATION = 14 | Metadata.Key.of(AUTHORIZATION_KEY, ASCII_STRING_MARSHALLER); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/data/GrpcJwtContext.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.data; 2 | 3 | import java.util.Optional; 4 | 5 | public class GrpcJwtContext { 6 | 7 | private GrpcJwtContext() { 8 | } 9 | 10 | private static final String CONTEXT_DATA = "context_data"; 11 | 12 | public static final io.grpc.Context.Key CONTEXT_DATA_KEY = io.grpc.Context.key(CONTEXT_DATA); 13 | 14 | public static Optional get() { 15 | return Optional.ofNullable(CONTEXT_DATA_KEY.get()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/data/JwtContextData.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.data; 2 | 3 | import io.jsonwebtoken.*; 4 | import lombok.AllArgsConstructor; 5 | 6 | import java.util.Set; 7 | 8 | @AllArgsConstructor 9 | public class JwtContextData { 10 | private final String jwt; 11 | private final String userId; 12 | private final Set roles; 13 | private final Claims jwtClaims; 14 | 15 | public String getJwt() { 16 | return jwt; 17 | } 18 | 19 | public String getUserId() { 20 | return userId; 21 | } 22 | 23 | public Set getRoles() { 24 | return roles; 25 | } 26 | 27 | public Claims getJwtClaims() { 28 | return jwtClaims; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/exception/AuthException.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.exception; 2 | 3 | public class AuthException extends RuntimeException { 4 | public AuthException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/exception/UnauthenticatedException.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.exception; 2 | 3 | public class UnauthenticatedException extends RuntimeException { 4 | public UnauthenticatedException(String message, Throwable cause) { 5 | super(message, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/interceptor/AllowedCollector.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.interceptor; 2 | 3 | import com.google.common.collect.Sets; 4 | import io.github.majusko.grpc.jwt.annotation.Allow; 5 | import io.github.majusko.grpc.jwt.annotation.Exposed; 6 | import io.github.majusko.grpc.jwt.data.AllowedMethod; 7 | import org.lognet.springboot.grpc.GRpcService; 8 | import org.springframework.beans.factory.config.BeanPostProcessor; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.lang.reflect.Method; 12 | import java.util.Arrays; 13 | import java.util.Map; 14 | import java.util.Optional; 15 | import java.util.Set; 16 | import java.util.stream.Collectors; 17 | 18 | @Component 19 | public class AllowedCollector implements BeanPostProcessor { 20 | 21 | private static final String GRPC_BASE_CLASS_NAME_EXT = "ImplBase"; 22 | private static final String PACKAGE_CLASS_DELIMITER = "."; 23 | private static final String CLASS_METHOD_DELIMITER = "/"; 24 | private static final String EMPTY_STRING = ""; 25 | 26 | private Map allowedMethods; 27 | private Map> exposedMethods; 28 | 29 | @Override 30 | public Object postProcessBeforeInitialization(Object bean, String beanName) { 31 | processGrpcServices(bean.getClass()); 32 | 33 | return bean; 34 | } 35 | 36 | @Override 37 | public Object postProcessAfterInitialization(Object bean, String beanName) { 38 | return bean; 39 | } 40 | 41 | Optional getAllowedAuth(String methodName) { 42 | return Optional.ofNullable(allowedMethods.get(methodName)); 43 | } 44 | 45 | Optional> getExposedEnv(String methodName) { 46 | return Optional.ofNullable(exposedMethods.get(methodName)); 47 | } 48 | 49 | private void processGrpcServices(Class beanClass) { 50 | if (beanClass.isAnnotationPresent(GRpcService.class)) { 51 | this.allowedMethods = findAllowedMethods(beanClass); 52 | this.exposedMethods = findExposedMethods(beanClass); 53 | } 54 | } 55 | 56 | private Map findAllowedMethods(Class beanClass) { 57 | return Arrays.stream(beanClass.getMethods()) 58 | .filter(method -> method.isAnnotationPresent(Allow.class)) 59 | .map(method -> buildAllowed(beanClass, method)) 60 | .collect(Collectors.toMap(AllowedMethod::getMethod, allowedMethod -> allowedMethod)); 61 | } 62 | 63 | private Map> findExposedMethods(Class beanClass) { 64 | return Arrays.stream(beanClass.getMethods()) 65 | .filter(method -> method.isAnnotationPresent(Exposed.class)) 66 | .collect(Collectors.toMap(method -> getGrpcServiceDescriptor(beanClass, method), this::buildEnv)); 67 | } 68 | 69 | private Set buildEnv(Method method) { 70 | final Exposed annotation = method.getAnnotation(Exposed.class); 71 | return Arrays.stream(annotation.environments()).collect(Collectors.toSet()); 72 | } 73 | 74 | private AllowedMethod buildAllowed(Class gRpcServiceClass, Method method) { 75 | final Allow annotation = method.getAnnotation(Allow.class); 76 | final Set roles = Sets.newHashSet(Arrays.asList(annotation.roles())); 77 | 78 | return new AllowedMethod(getGrpcServiceDescriptor(gRpcServiceClass, method), annotation.ownerField(), roles); 79 | } 80 | 81 | private String getGrpcServiceDescriptor(Class gRpcServiceClass, Method method) { 82 | final Class superClass = gRpcServiceClass.getSuperclass(); 83 | 84 | return (superClass.getPackage().getName() + 85 | PACKAGE_CLASS_DELIMITER + 86 | superClass.getSimpleName().replace(GRPC_BASE_CLASS_NAME_EXT, EMPTY_STRING) + 87 | CLASS_METHOD_DELIMITER + 88 | method.getName()).toLowerCase(); 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/interceptor/AuthClientInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.interceptor; 2 | 3 | import io.github.majusko.grpc.jwt.data.GrpcHeader; 4 | import io.github.majusko.grpc.jwt.service.JwtService; 5 | import io.grpc.*; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | public class AuthClientInterceptor implements ClientInterceptor { 10 | 11 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 12 | 13 | private final JwtService jwtService; 14 | 15 | public AuthClientInterceptor(JwtService jwtService) { 16 | this.jwtService = jwtService; 17 | } 18 | 19 | @Override 20 | public ClientCall interceptCall( 21 | MethodDescriptor method, 22 | CallOptions callOptions, 23 | Channel next 24 | ) { 25 | return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { 26 | 27 | @Override 28 | public void start(Listener responseListener, final Metadata metadata) { 29 | final Listener tracingResponseListener = responseListener(responseListener); 30 | 31 | super.start(tracingResponseListener, injectInternalToken(metadata)); 32 | } 33 | }; 34 | } 35 | 36 | private ForwardingClientCallListener responseListener(ClientCall.Listener responseListener) { 37 | return new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { 38 | @Override 39 | public void onClose(Status status, Metadata metadata) { 40 | handleAuthStatusCodes(status); 41 | 42 | super.onClose(status, metadata); 43 | } 44 | }; 45 | } 46 | 47 | private Metadata injectInternalToken(Metadata metadata) { 48 | final String authHeader = metadata.get(GrpcHeader.AUTHORIZATION); 49 | 50 | if(authHeader == null || authHeader.isEmpty()) { 51 | final String internalToken = jwtService.getInternal(); 52 | metadata.put(GrpcHeader.AUTHORIZATION, internalToken); 53 | } 54 | 55 | return metadata; 56 | } 57 | 58 | private void handleAuthStatusCodes(Status status) { 59 | if(status.getCode().equals(Status.UNAUTHENTICATED.getCode())) { 60 | logger.error("Grpc call is unauthenticated.", status.getCause()); 61 | } 62 | 63 | if(status.getCode().equals(Status.PERMISSION_DENIED.getCode())) { 64 | logger.error("Grpc call is unauthorized.", status.getCause()); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/interceptor/AuthServerInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.interceptor; 2 | 3 | import com.google.common.collect.Sets; 4 | import io.github.majusko.grpc.jwt.data.AllowedMethod; 5 | import io.github.majusko.grpc.jwt.data.JwtContextData; 6 | import io.github.majusko.grpc.jwt.data.GrpcHeader; 7 | import io.github.majusko.grpc.jwt.data.GrpcJwtContext; 8 | import io.github.majusko.grpc.jwt.exception.AuthException; 9 | import io.github.majusko.grpc.jwt.exception.UnauthenticatedException; 10 | import io.github.majusko.grpc.jwt.service.JwtService; 11 | import io.grpc.*; 12 | import io.jsonwebtoken.Claims; 13 | import io.jsonwebtoken.JwtException; 14 | import io.jsonwebtoken.Jwts; 15 | import org.lognet.springboot.grpc.GRpcGlobalInterceptor; 16 | import org.springframework.core.env.Environment; 17 | 18 | import java.lang.reflect.Field; 19 | import java.util.*; 20 | import java.util.stream.Collectors; 21 | 22 | @GRpcGlobalInterceptor 23 | public class AuthServerInterceptor implements ServerInterceptor { 24 | 25 | private static final String GRPC_FIELD_MODIFIER = "_"; 26 | private static final String BEARER = "Bearer"; 27 | private static final ServerCall.Listener NOOP_LISTENER = new ServerCall.Listener() { 28 | }; 29 | 30 | private final AllowedCollector allowedCollector; 31 | private final JwtService jwtService; 32 | private final Environment environment; 33 | 34 | public AuthServerInterceptor( 35 | AllowedCollector allowedCollector, 36 | JwtService jwtService, 37 | Environment environment 38 | ) { 39 | this.allowedCollector = allowedCollector; 40 | this.jwtService = jwtService; 41 | this.environment = environment; 42 | } 43 | 44 | @Override 45 | public ServerCall.Listener interceptCall( 46 | ServerCall call, Metadata metadata, ServerCallHandler next 47 | ) { 48 | try { 49 | final JwtContextData contextData = parseAuthContextData(metadata); 50 | final Context context = Context.current().withValue(GrpcJwtContext.CONTEXT_DATA_KEY, contextData); 51 | 52 | return buildListener(call, metadata, next, context, contextData); 53 | } catch(UnauthenticatedException e) { 54 | call.close(Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e.getCause()), metadata); 55 | //noinspection unchecked 56 | return NOOP_LISTENER; 57 | } 58 | } 59 | 60 | private ForwardingServerCallListener buildListener( 61 | ServerCall call, 62 | Metadata metadata, 63 | ServerCallHandler next, 64 | Context context, 65 | JwtContextData contextData 66 | ) { 67 | final ServerCall.Listener customDelegate = Contexts.interceptCall(context, call, metadata, next); 68 | 69 | return new ForwardingServerCallListener() { 70 | 71 | @SuppressWarnings("unchecked") 72 | ServerCall.Listener delegate = NOOP_LISTENER; 73 | 74 | @Override 75 | protected ServerCall.Listener delegate() { 76 | return delegate; 77 | } 78 | 79 | @Override 80 | public void onMessage(ReqT request) { 81 | try { 82 | if(delegate == NOOP_LISTENER) { 83 | final String methodName = call.getMethodDescriptor().getFullMethodName().toLowerCase(); 84 | 85 | validateAnnotatedMethods(request, contextData, methodName); 86 | 87 | delegate = customDelegate; 88 | } 89 | } catch(AuthException e) { 90 | call.close(Status.PERMISSION_DENIED 91 | .withDescription(e.getMessage()) 92 | .withCause(e.getCause()), metadata); 93 | } 94 | super.onMessage(request); 95 | } 96 | }; 97 | } 98 | 99 | private void validateAnnotatedMethods(ReqT request, JwtContextData contextData, String methodName) { 100 | if(!validateExposedAnnotation(contextData, methodName)) { 101 | validateAllowedAnnotation(request, contextData, methodName); 102 | } 103 | } 104 | 105 | @SuppressWarnings("unchecked") 106 | private boolean validateExposedAnnotation(JwtContextData contextData, String methodName) { 107 | final Set exposedToEnvironments = allowedCollector.getExposedEnv(methodName).orElse(Sets.newHashSet()); 108 | final boolean methodIsExposed = Arrays.stream(environment.getActiveProfiles()) 109 | .anyMatch(exposedToEnvironments::contains); 110 | 111 | if(methodIsExposed) { 112 | if(contextData == null) throw new AuthException("Missing JWT data."); 113 | 114 | final List rawEnvironments = (List) contextData 115 | .getJwtClaims().get(JwtService.TOKEN_ENV, List.class); 116 | final Set environments = rawEnvironments.stream().map(Object::toString).collect(Collectors.toSet()); 117 | 118 | return exposedToEnvironments.stream().anyMatch(environments::contains); 119 | } 120 | 121 | return false; 122 | } 123 | 124 | private void validateAllowedAnnotation(ReqT request, JwtContextData contextData, String methodName) { 125 | allowedCollector.getAllowedAuth(methodName) 126 | .ifPresent(value -> authorizeOwnerOrRoles(request, contextData, value)); 127 | } 128 | 129 | private void authorizeOwnerOrRoles(ReqT request, JwtContextData contextData, AllowedMethod allowedMethod) { 130 | if(contextData == null) throw new AuthException("Missing JWT data."); 131 | if(allowedMethod.getOwnerField().isEmpty()) { 132 | validateRoles(new HashSet<>(allowedMethod.getRoles()), contextData.getRoles()); 133 | } else { 134 | authorizeOwner(request, contextData, allowedMethod); 135 | } 136 | } 137 | 138 | private String parseOwner(ReqT request, String fieldName) { 139 | try { 140 | final Field field = request.getClass().getDeclaredField(fieldName + GRPC_FIELD_MODIFIER); 141 | field.setAccessible(true); 142 | return String.valueOf(field.get(request)); 143 | } catch(NoSuchFieldException | IllegalAccessException e) { 144 | throw new AuthException("Missing owner field."); 145 | } 146 | } 147 | 148 | private void authorizeOwner(ReqT request, JwtContextData jwtContext, AllowedMethod allowedMethod) { 149 | final String uid = parseOwner(request, allowedMethod.getOwnerField()); 150 | 151 | if(!jwtContext.getUserId().equals(uid)) validateRoles(new HashSet<>(allowedMethod.getRoles()), jwtContext.getRoles()); 152 | } 153 | 154 | private void validateRoles(Set requiredRoles, Set userRoles) { 155 | 156 | if(requiredRoles.isEmpty()) { 157 | throw new AuthException("Endpoint does not have specified roles."); 158 | } 159 | 160 | requiredRoles.retainAll(Objects.requireNonNull(userRoles)); 161 | 162 | if(requiredRoles.isEmpty()) { 163 | throw new AuthException("Missing required permission roles."); 164 | } 165 | } 166 | 167 | @SuppressWarnings("unchecked") 168 | private JwtContextData parseAuthContextData(Metadata metadata) { 169 | try { 170 | final String authHeaderData = metadata.get(GrpcHeader.AUTHORIZATION); 171 | 172 | if(authHeaderData == null) { 173 | return null; 174 | } 175 | 176 | final String token = authHeaderData.replace(BEARER, "").trim(); 177 | final Claims jwtBody = Jwts.parser().setSigningKey(jwtService.getKey()).parseClaimsJws(token).getBody(); 178 | final List roles = (List) jwtBody.get(JwtService.JWT_ROLES, List.class); 179 | 180 | return new JwtContextData(token, jwtBody.getSubject(), Sets.newHashSet(roles), jwtBody); 181 | } catch(JwtException | IllegalArgumentException e) { 182 | throw new UnauthenticatedException(e.getMessage(), e); 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/service/GrpcRole.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.service; 2 | 3 | public class GrpcRole { 4 | private GrpcRole(){} 5 | public static final String INTERNAL = "internal_role"; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/service/JwtService.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.service; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.google.common.collect.Sets; 5 | import io.github.majusko.grpc.jwt.GrpcJwtProperties; 6 | import io.github.majusko.grpc.jwt.service.dto.JwtData; 7 | import io.github.majusko.grpc.jwt.service.dto.JwtMetadata; 8 | import io.github.majusko.grpc.jwt.service.dto.JwtToken; 9 | import io.jsonwebtoken.Claims; 10 | import io.jsonwebtoken.Jwts; 11 | import org.apache.commons.codec.digest.DigestUtils; 12 | import org.springframework.core.env.Environment; 13 | 14 | import javax.crypto.SecretKey; 15 | import javax.crypto.spec.SecretKeySpec; 16 | import java.time.LocalDateTime; 17 | import java.time.ZoneId; 18 | import java.util.Arrays; 19 | import java.util.Base64; 20 | import java.util.Date; 21 | import java.util.stream.Collectors; 22 | 23 | public class JwtService { 24 | 25 | public static final String TOKEN_ENV = "token_env"; 26 | public static final String JWT_ROLES = "jwt_roles"; 27 | 28 | private static final String INTERNAL_ACCOUNT = "internal_account"; 29 | private static final Double REFRESH_TIME_THRESHOLD = 0.2; 30 | 31 | private final GrpcJwtProperties properties; 32 | 33 | private JwtMetadata metadata; 34 | private JwtToken internal; 35 | 36 | public JwtService(Environment env, GrpcJwtProperties properties) { 37 | this.properties = properties; 38 | this.metadata = JwtMetadata.builder() 39 | .env(Arrays.stream(env.getActiveProfiles()).collect(Collectors.toList())) 40 | .expirationSec(properties.getExpirationSec()) 41 | .key(generateKey(properties.getSecret(), properties.getAlgorithm())) 42 | .build(); 43 | this.internal = generateInternalToken(properties.getExpirationSec(), metadata); 44 | } 45 | 46 | /** 47 | * Generate fresh JWT token with specified roles and userId. 48 | * @param data JwtData with data needed for JWT token generation. 49 | * @return String version of your new JWT token 50 | */ 51 | public String generate(JwtData data) { 52 | return generateJwt(data, metadata); 53 | } 54 | 55 | /** 56 | * Get the internal JWT token and automatically refresh the token if it's expired. 57 | * This token is used for inter-service communication. 58 | * @return String version of your internal JWT token. 59 | */ 60 | public String getInternal() { 61 | final double refreshThresholdValue = properties.getExpirationSec() * REFRESH_TIME_THRESHOLD; 62 | 63 | if (LocalDateTime.now().plusSeconds((long) refreshThresholdValue).isAfter(internal.getExpiration())) { 64 | refreshInternalToken(); 65 | } 66 | 67 | return internal.getToken(); 68 | } 69 | 70 | /** 71 | * Get the key used for JWT token generation. 72 | * @return generated SecretKey with configuration from application.properties. 73 | */ 74 | public SecretKey getKey() { 75 | return metadata.getKey(); 76 | } 77 | 78 | private SecretKeySpec generateKey(String signingSecret, String signAlgorithm) { 79 | final String sha256hex = DigestUtils.sha256Hex(signingSecret); 80 | final byte[] decodedKey = Base64.getDecoder().decode(sha256hex); 81 | return new SecretKeySpec(decodedKey, 0, decodedKey.length, signAlgorithm); 82 | } 83 | 84 | private String generateJwt(JwtData data, JwtMetadata metadata) { 85 | final LocalDateTime future = LocalDateTime.now().plusSeconds(metadata.getExpirationSec()); 86 | final Claims ourClaims = Jwts.claims(); 87 | 88 | ourClaims.put(JWT_ROLES, Lists.newArrayList(data.getRoles())); 89 | ourClaims.put(TOKEN_ENV, metadata.getEnv()); 90 | 91 | return Jwts.builder() 92 | .setClaims(ourClaims) 93 | .setSubject(data.getUserId()) 94 | .setIssuedAt(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant())) 95 | .setExpiration(Date.from(future.atZone(ZoneId.systemDefault()).toInstant())) 96 | .signWith(metadata.getKey()).compact(); 97 | } 98 | 99 | private void refreshInternalToken() { 100 | this.internal = generateInternalToken(properties.getExpirationSec(), metadata); 101 | } 102 | 103 | private JwtToken generateInternalToken(Long expirationSec, JwtMetadata jwtMetadata) { 104 | return new JwtToken( 105 | generateJwt(new JwtData(INTERNAL_ACCOUNT, Sets.newHashSet(GrpcRole.INTERNAL)), jwtMetadata), 106 | LocalDateTime.now().plusSeconds(expirationSec) 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/service/dto/JwtData.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.service.dto; 2 | 3 | import com.google.common.collect.Sets; 4 | 5 | import java.util.Objects; 6 | import java.util.Set; 7 | 8 | public class JwtData { 9 | private final String userId; 10 | private final Set roles; 11 | 12 | public JwtData(String userId, String role) { 13 | this(userId, Sets.newHashSet(Objects.requireNonNull(role))); 14 | } 15 | 16 | public JwtData(String userId, Set roles) { 17 | this.userId = Objects.requireNonNull(userId); 18 | this.roles = Objects.requireNonNull(roles); 19 | } 20 | 21 | public String getUserId() { 22 | return userId; 23 | } 24 | 25 | public Set getRoles() { 26 | return roles; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/service/dto/JwtMetadata.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.service.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | 6 | import javax.crypto.SecretKey; 7 | import java.util.List; 8 | 9 | @Builder 10 | @AllArgsConstructor 11 | public class JwtMetadata { 12 | private Long expirationSec; 13 | private SecretKey key; 14 | private List env; 15 | 16 | public Long getExpirationSec() { 17 | return expirationSec; 18 | } 19 | 20 | public SecretKey getKey() { 21 | return key; 22 | } 23 | 24 | public List getEnv() { 25 | return env; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/github/majusko/grpc/jwt/service/dto/JwtToken.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt.service.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @AllArgsConstructor 8 | public class JwtToken { 9 | private String token; 10 | private LocalDateTime expiration; 11 | 12 | public String getToken() { 13 | return token; 14 | } 15 | 16 | public LocalDateTime getExpiration() { 17 | return expiration; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.github.majusko.grpc.jwt.GrpcJwtAutoConfiguration -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | grpc.jwt.algorithm=HmacSHA256 2 | grpc.jwt.secret=secret 3 | grpc.jwt.expirationSec=3600 4 | grpc.jwt.internal.refreshSec=60 -------------------------------------------------------------------------------- /src/test/java/io/github/majusko/grpc/jwt/GrpcJwtSpringBootStarterApplicationTest.java: -------------------------------------------------------------------------------- 1 | package io.github.majusko.grpc.jwt; 2 | 3 | import com.google.common.collect.Sets; 4 | import com.google.protobuf.Empty; 5 | import io.github.majusko.grpc.jwt.annotation.Allow; 6 | import io.github.majusko.grpc.jwt.annotation.Exposed; 7 | import io.github.majusko.grpc.jwt.data.GrpcHeader; 8 | import io.github.majusko.grpc.jwt.data.GrpcJwtContext; 9 | import io.github.majusko.grpc.jwt.data.JwtContextData; 10 | import io.github.majusko.grpc.jwt.interceptor.AllowedCollector; 11 | import io.github.majusko.grpc.jwt.interceptor.AuthClientInterceptor; 12 | import io.github.majusko.grpc.jwt.interceptor.AuthServerInterceptor; 13 | import io.github.majusko.grpc.jwt.interceptor.proto.Example; 14 | import io.github.majusko.grpc.jwt.interceptor.proto.ExampleServiceGrpc; 15 | import io.github.majusko.grpc.jwt.service.GrpcRole; 16 | import io.github.majusko.grpc.jwt.service.JwtService; 17 | import io.github.majusko.grpc.jwt.service.dto.JwtData; 18 | import io.grpc.*; 19 | import io.grpc.inprocess.InProcessChannelBuilder; 20 | import io.grpc.inprocess.InProcessServerBuilder; 21 | import io.grpc.stub.MetadataUtils; 22 | import io.grpc.stub.StreamObserver; 23 | import io.grpc.testing.GrpcCleanupRule; 24 | import org.junit.Rule; 25 | import org.junit.jupiter.api.Assertions; 26 | import org.junit.jupiter.api.Test; 27 | import org.lognet.springboot.grpc.GRpcService; 28 | import org.springframework.beans.factory.annotation.Autowired; 29 | import org.springframework.boot.test.context.SpringBootTest; 30 | import org.springframework.core.env.Environment; 31 | import org.springframework.test.context.ActiveProfiles; 32 | 33 | import java.io.IOException; 34 | import java.lang.reflect.Field; 35 | import java.lang.reflect.InvocationTargetException; 36 | import java.lang.reflect.Method; 37 | 38 | @SpringBootTest 39 | @ActiveProfiles("test") 40 | public class GrpcJwtSpringBootStarterApplicationTest { 41 | 42 | @Autowired 43 | private Environment environment; 44 | 45 | @Autowired 46 | private JwtService jwtService; 47 | 48 | @Autowired 49 | private AllowedCollector allowedCollector; 50 | 51 | @Autowired 52 | private AuthServerInterceptor authServerInterceptor; 53 | 54 | @Autowired 55 | private AuthClientInterceptor authClientInterceptor; 56 | 57 | @Rule 58 | public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); 59 | 60 | @Test 61 | public void testSuccessInternalToken() throws IOException { 62 | 63 | final ManagedChannel channel = initTestServer(new ExampleService()); 64 | final Channel interceptedChannel = ClientInterceptors.intercept(channel, authClientInterceptor); 65 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = 66 | ExampleServiceGrpc.newBlockingStub(interceptedChannel); 67 | final Empty response = stub.getExample(Example.GetExampleRequest.newBuilder().build()); 68 | 69 | Assertions.assertNotNull(response); 70 | } 71 | 72 | @Test 73 | public void testSuccessCustomAdminToken() throws IOException { 74 | 75 | final String token = jwtService.generate(new JwtData("some-user-id", "admin")); 76 | 77 | final ManagedChannel channel = initTestServer(new ExampleService()); 78 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 79 | 80 | final Metadata header = new Metadata(); 81 | header.put(GrpcHeader.AUTHORIZATION, token); 82 | 83 | final ExampleServiceGrpc.ExampleServiceBlockingStub injectedStub = MetadataUtils.attachHeaders(stub, header); 84 | final Empty response = injectedStub.getExample(Example.GetExampleRequest.newBuilder().build()); 85 | 86 | Assertions.assertNotNull(response); 87 | } 88 | 89 | @Test 90 | public void testCustomTokenWithWrongRole() throws IOException { 91 | 92 | final String token = jwtService.generate(new JwtData("some-user-id", "non-existing-role")); 93 | 94 | final ManagedChannel channel = initTestServer(new ExampleService()); 95 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 96 | 97 | final Metadata header = new Metadata(); 98 | header.put(GrpcHeader.AUTHORIZATION, token); 99 | 100 | final ExampleServiceGrpc.ExampleServiceBlockingStub injectedStub = MetadataUtils.attachHeaders(stub, header); 101 | 102 | Status status = Status.OK; 103 | 104 | try { 105 | final Empty ignored = injectedStub.getExample(Example.GetExampleRequest.newBuilder().build()); 106 | } catch (StatusRuntimeException e) { 107 | status = e.getStatus(); 108 | } 109 | 110 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 111 | } 112 | 113 | @Test 114 | public void testWrongToken() throws IOException { 115 | 116 | String token = jwtService.generate(new JwtData("some-user-id", "non-existing-role")); 117 | 118 | token += "crwvvef"; 119 | final ManagedChannel channel = initTestServer(new ExampleService()); 120 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 121 | 122 | final Metadata header = new Metadata(); 123 | header.put(GrpcHeader.AUTHORIZATION, token); 124 | 125 | final ExampleServiceGrpc.ExampleServiceBlockingStub injectedStub = MetadataUtils.attachHeaders(stub, header); 126 | 127 | Status status = Status.OK; 128 | 129 | try { 130 | final Empty ignored = injectedStub.getExample(Example.GetExampleRequest.newBuilder().build()); 131 | } catch (StatusRuntimeException e) { 132 | status = e.getStatus(); 133 | } 134 | 135 | Assertions.assertEquals(Status.UNAUTHENTICATED.getCode(), status.getCode()); 136 | } 137 | 138 | @Test 139 | public void testMissingAuth() throws IOException { 140 | final ManagedChannel channel = initTestServer(new ExampleService()); 141 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 142 | 143 | Status status = Status.OK; 144 | 145 | try { 146 | final Empty ignored = stub.getExample(Example.GetExampleRequest.newBuilder().build()); 147 | } catch (StatusRuntimeException e) { 148 | status = e.getStatus(); 149 | } 150 | 151 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 152 | } 153 | 154 | @Test 155 | public void testMissingAuthForSimpleAllow() throws IOException { 156 | final ManagedChannel channel = initTestServer(new ExampleService()); 157 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 158 | 159 | Status status = Status.OK; 160 | 161 | try { 162 | final Empty ignored = stub.someAction(Example.GetExampleRequest.newBuilder().build()); 163 | } catch (StatusRuntimeException e) { 164 | status = e.getStatus(); 165 | } 166 | 167 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 168 | } 169 | 170 | @Test 171 | public void testMissingCredentials() throws IOException { 172 | 173 | final ManagedChannel channel = initTestServer(new ExampleService()); 174 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 175 | 176 | Status status = Status.OK; 177 | 178 | try { 179 | final Empty ignored = stub.getExample(Example.GetExampleRequest.newBuilder().build()); 180 | } catch (StatusRuntimeException e) { 181 | status = e.getStatus(); 182 | } 183 | 184 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 185 | } 186 | 187 | @Test 188 | public void testCustomTokenWithWrongRoleButMatchingOwner() throws IOException { 189 | final String ownerUserId = "matching-user-id"; 190 | final String token = jwtService.generate(new JwtData(ownerUserId, "non-existing-role")); 191 | 192 | final ManagedChannel channel = initTestServer(new ExampleService()); 193 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 194 | 195 | final Metadata header = new Metadata(); 196 | header.put(GrpcHeader.AUTHORIZATION, token); 197 | 198 | final ExampleServiceGrpc.ExampleServiceBlockingStub injectedStub = MetadataUtils.attachHeaders(stub, header); 199 | final Example.GetExampleRequest request = Example.GetExampleRequest.newBuilder() 200 | .setUserId(ownerUserId).build(); 201 | final Empty response = injectedStub.getExample(request); 202 | 203 | Assertions.assertNotNull(response); 204 | } 205 | 206 | @Test 207 | public void testAllowAnnotationWithMissingInterceptor() throws IOException { 208 | final ManagedChannel channel = initTestServer(new ExampleService()); 209 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 210 | 211 | Status status = Status.OK; 212 | 213 | try { 214 | final Empty ignored = stub.getExample(Example.GetExampleRequest.newBuilder().build()); 215 | } catch (StatusRuntimeException e) { 216 | status = e.getStatus(); 217 | } 218 | 219 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 220 | } 221 | 222 | @Test 223 | public void testExposeAnnotationWithMissingInterceptor() throws IOException { 224 | final ManagedChannel channel = initTestServer(new ExampleService()); 225 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 226 | 227 | Status status = Status.OK; 228 | 229 | try { 230 | final Empty ignored = stub.listExample(Example.GetExampleRequest.newBuilder().build()); 231 | } catch (StatusRuntimeException e) { 232 | status = e.getStatus(); 233 | } 234 | 235 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 236 | } 237 | 238 | @Test 239 | public void testSuccessExposeToTestEnvAnnotation() throws IOException { 240 | final ManagedChannel channel = initTestServer(new ExampleService()); 241 | final Channel interceptedChannel = ClientInterceptors.intercept(channel, authClientInterceptor); 242 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = 243 | ExampleServiceGrpc.newBlockingStub(interceptedChannel); 244 | 245 | final Empty response = stub.listExample(Example.GetExampleRequest.newBuilder().build()); 246 | 247 | Assertions.assertNotNull(response); 248 | } 249 | 250 | @Test 251 | public void testNonExistingFieldInPayload() throws IOException { 252 | final ManagedChannel channel = initTestServer(new ExampleService()); 253 | final Channel interceptedChannel = ClientInterceptors.intercept(channel, authClientInterceptor); 254 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = 255 | ExampleServiceGrpc.newBlockingStub(interceptedChannel); 256 | 257 | Status status = Status.OK; 258 | 259 | try { 260 | final Empty ignored = stub.saveExample(Empty.getDefaultInstance()); 261 | } catch (StatusRuntimeException e) { 262 | status = e.getStatus(); 263 | } 264 | 265 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 266 | } 267 | 268 | @Test 269 | public void testDiffUserIdAndNonExistingRole() throws IOException { 270 | final ManagedChannel channel = initTestServer(new ExampleService()); 271 | final Channel interceptedChannel = ClientInterceptors.intercept(channel, authClientInterceptor); 272 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = 273 | ExampleServiceGrpc.newBlockingStub(interceptedChannel); 274 | 275 | Status status = Status.OK; 276 | 277 | try { 278 | final Empty ignored = stub.deleteExample(Example.GetExampleRequest.getDefaultInstance()); 279 | } catch (StatusRuntimeException e) { 280 | status = e.getStatus(); 281 | } 282 | 283 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 284 | } 285 | 286 | @Test 287 | public void testCustomTokenWithEmptyUserIdAndEmptyRoles() throws IOException { 288 | final String token = jwtService.generate(new JwtData("random-user-id", Sets.newHashSet())); 289 | 290 | final ManagedChannel channel = initTestServer(new ExampleService()); 291 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 292 | 293 | final Metadata header = new Metadata(); 294 | header.put(GrpcHeader.AUTHORIZATION, token); 295 | 296 | final ExampleServiceGrpc.ExampleServiceBlockingStub injectedStub = MetadataUtils.attachHeaders(stub, header); 297 | final Example.GetExampleRequest request = Example.GetExampleRequest.newBuilder() 298 | .setUserId("other-user-id").build(); 299 | 300 | Status status = Status.OK; 301 | 302 | try { 303 | final Empty ignore = injectedStub.getExample(request); 304 | } catch (StatusRuntimeException e) { 305 | status = e.getStatus(); 306 | } 307 | 308 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 309 | } 310 | 311 | @Test 312 | public void testEmptyUserIdInToken() throws IOException { 313 | final String token = jwtService.generate(new JwtData("", Sets.newHashSet("some-other-role"))); 314 | final ManagedChannel channel = initTestServer(new ExampleService()); 315 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 316 | final Metadata header = new Metadata(); 317 | 318 | header.put(GrpcHeader.AUTHORIZATION, token); 319 | 320 | final ExampleServiceGrpc.ExampleServiceBlockingStub injectedStub = MetadataUtils.attachHeaders(stub, header); 321 | final Example.GetExampleRequest request = Example.GetExampleRequest.newBuilder() 322 | .setUserId("other-user-id").build(); 323 | 324 | Status status = Status.OK; 325 | 326 | try { 327 | final Empty ignore = injectedStub.getExample(request); 328 | } catch (StatusRuntimeException e) { 329 | status = e.getStatus(); 330 | } 331 | 332 | Assertions.assertEquals(Status.PERMISSION_DENIED.getCode(), status.getCode()); 333 | } 334 | 335 | @Test 336 | public void testExpiredInternalToken() throws IOException, NoSuchFieldException, IllegalAccessException, 337 | NoSuchMethodException, InvocationTargetException, InterruptedException { 338 | 339 | final GrpcJwtProperties customProperties = new GrpcJwtProperties(); 340 | final Field field = customProperties.getClass().getDeclaredField("expirationSec"); 341 | field.setAccessible(true); 342 | field.set(customProperties, 1L); 343 | 344 | final Field propertyField = jwtService.getClass().getDeclaredField("properties"); 345 | propertyField.setAccessible(true); 346 | final GrpcJwtProperties existingProperties = (GrpcJwtProperties) propertyField.get(jwtService); 347 | propertyField.set(jwtService, customProperties); 348 | 349 | final Method refreshMethod = jwtService.getClass().getDeclaredMethod("refreshInternalToken"); 350 | refreshMethod.setAccessible(true); 351 | 352 | refreshMethod.invoke(jwtService); 353 | 354 | final ManagedChannel channel = initTestServer(new ExampleService()); 355 | final Channel interceptedChannel = ClientInterceptors.intercept(channel, authClientInterceptor); 356 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = 357 | ExampleServiceGrpc.newBlockingStub(interceptedChannel); 358 | final Example.GetExampleRequest request = Example.GetExampleRequest.newBuilder() 359 | .setUserId("other-user-id").build(); 360 | 361 | Thread.sleep(2000); 362 | 363 | final Empty response = stub.getExample(request); 364 | 365 | Assertions.assertNotNull(response); 366 | 367 | propertyField.set(jwtService, existingProperties); 368 | refreshMethod.invoke(jwtService); 369 | } 370 | 371 | @Test 372 | public void testExpiredToken() throws IOException, NoSuchFieldException, IllegalAccessException { 373 | 374 | final GrpcJwtProperties customProperties = new GrpcJwtProperties(); 375 | final Field field = customProperties.getClass().getDeclaredField("expirationSec"); 376 | field.setAccessible(true); 377 | field.set(customProperties, -10L); 378 | 379 | final JwtService customJwtService = new JwtService(environment, customProperties); 380 | final String token = customJwtService.generate(new JwtData("lala", Sets.newHashSet(ExampleService.ADMIN))); 381 | 382 | final ManagedChannel channel = initTestServer(new ExampleService()); 383 | final Channel interceptedChannel = ClientInterceptors.intercept(channel, authClientInterceptor); 384 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = 385 | ExampleServiceGrpc.newBlockingStub(interceptedChannel); 386 | 387 | final Metadata header = new Metadata(); 388 | header.put(GrpcHeader.AUTHORIZATION, token); 389 | 390 | final ExampleServiceGrpc.ExampleServiceBlockingStub injectedStub = MetadataUtils.attachHeaders(stub, header); 391 | final Example.GetExampleRequest request = Example.GetExampleRequest.newBuilder() 392 | .setUserId("other-user-id").build(); 393 | 394 | Status status = Status.OK; 395 | 396 | try { 397 | final Empty ignore = injectedStub.getExample(request); 398 | } catch (StatusRuntimeException e) { 399 | status = e.getStatus(); 400 | } 401 | 402 | Assertions.assertEquals(Status.UNAUTHENTICATED.getCode(), status.getCode()); 403 | } 404 | 405 | @Test 406 | public void testMissingOwnerFieldInAnnotationSoRolesAreValidated() throws IOException { 407 | final String token = jwtService 408 | .generate(new JwtData("random-user-id", Sets.newHashSet(ExampleService.ADMIN))); 409 | 410 | final ManagedChannel channel = initTestServer(new ExampleService()); 411 | final ExampleServiceGrpc.ExampleServiceBlockingStub stub = ExampleServiceGrpc.newBlockingStub(channel); 412 | 413 | final Metadata header = new Metadata(); 414 | header.put(GrpcHeader.AUTHORIZATION, token); 415 | 416 | final ExampleServiceGrpc.ExampleServiceBlockingStub injectedStub = MetadataUtils.attachHeaders(stub, header); 417 | final Example.GetExampleRequest request = Example.GetExampleRequest.newBuilder() 418 | .setUserId("other-user-id").build(); 419 | 420 | final Empty response = injectedStub.someAction(request); 421 | 422 | Assertions.assertNotNull(response); 423 | } 424 | 425 | private ManagedChannel initTestServer(BindableService service) throws IOException { 426 | 427 | final String serverName = InProcessServerBuilder.generateName(); 428 | final Server server = InProcessServerBuilder 429 | .forName(serverName).directExecutor() 430 | .addService(service) 431 | .intercept(authServerInterceptor) 432 | .build().start(); 433 | 434 | allowedCollector.postProcessBeforeInitialization(service, "exampleService"); 435 | 436 | grpcCleanup.register(server); 437 | 438 | return grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); 439 | } 440 | } 441 | 442 | @GRpcService 443 | class ExampleService extends ExampleServiceGrpc.ExampleServiceImplBase { 444 | 445 | public static final String ADMIN = "admin"; 446 | 447 | @Override 448 | @Allow(ownerField = "userId", roles = {GrpcRole.INTERNAL, ADMIN}) 449 | public void getExample(Example.GetExampleRequest request, StreamObserver response) { 450 | 451 | JwtContextData authContext = GrpcJwtContext.get().orElseThrow(RuntimeException::new); 452 | 453 | Assertions.assertNotNull(authContext.getJwt()); 454 | Assertions.assertTrue(authContext.getJwtClaims().size() > 0); 455 | 456 | if (!request.getUserId().equals(authContext.getUserId())) { 457 | Assertions.assertTrue(authContext.getRoles().stream() 458 | .anyMatch($ -> $.equals(GrpcRole.INTERNAL) || $.equals(ADMIN))); 459 | } 460 | 461 | response.onNext(Empty.getDefaultInstance()); 462 | response.onCompleted(); 463 | } 464 | 465 | @Override 466 | @Exposed(environments = "test") 467 | public void listExample(Example.GetExampleRequest request, StreamObserver response) { 468 | 469 | response.onNext(Empty.getDefaultInstance()); 470 | response.onCompleted(); 471 | } 472 | 473 | @Override 474 | @Allow(ownerField = "nonExistingField") 475 | public void saveExample(Empty request, StreamObserver response) { 476 | 477 | response.onNext(Empty.getDefaultInstance()); 478 | response.onCompleted(); 479 | } 480 | 481 | @Override 482 | @Allow(ownerField = "userId") 483 | public void deleteExample(Example.GetExampleRequest request, StreamObserver response) { 484 | 485 | response.onNext(Empty.getDefaultInstance()); 486 | response.onCompleted(); 487 | } 488 | 489 | @Override 490 | @Allow(roles = {ADMIN}) 491 | public void someAction(Example.GetExampleRequest request, StreamObserver response) { 492 | 493 | response.onNext(Empty.getDefaultInstance()); 494 | response.onCompleted(); 495 | } 496 | } -------------------------------------------------------------------------------- /src/test/proto/Example.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package io.github.majusko.grpc.jwt.interceptor.proto; 3 | 4 | import "google/protobuf/empty.proto"; 5 | 6 | service ExampleService { 7 | rpc GetExample (GetExampleRequest) returns (google.protobuf.Empty); 8 | rpc ListExample (GetExampleRequest) returns (google.protobuf.Empty); 9 | rpc SaveExample (google.protobuf.Empty) returns (google.protobuf.Empty); 10 | rpc DeleteExample (GetExampleRequest) returns (google.protobuf.Empty); 11 | rpc SomeAction (GetExampleRequest) returns (google.protobuf.Empty); 12 | } 13 | 14 | message GetExampleRequest { 15 | string userId = 1; 16 | int32 data = 2; 17 | } -------------------------------------------------------------------------------- /travis-settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | ossrh 8 | ${env.SONATYPE_USERNAME} 9 | ${env.SONATYPE_PASSWORD} 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------