├── .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 | [](https://search.maven.org/search?q=g:io.github.majusko)
4 | [](https://jitpack.io/#majusko/grpc-jwt-spring-boot-starter)
5 | [](https://travis-ci.com/majusko/grpc-jwt-spring-boot-starter)
6 | [](https://codecov.io/gh/majusko/grpc-jwt-spring-boot-starter/branch/master)
7 | [](https://opensource.org/licenses/MIT) [](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 |
--------------------------------------------------------------------------------