From a291890859d672464312f01ef7bd54ce53c328d5 Mon Sep 17 00:00:00 2001 From: brianwyka Date: Mon, 22 Feb 2021 12:15:21 -0500 Subject: [PATCH 01/12] Initial server security JWT implementation --- gradle.properties | 6 +- grpc-client-runtime/build.gradle | 4 +- grpc-server-runtime/build.gradle | 4 +- grpc-server-security-jwt/build.gradle | 20 +++ .../GrpcServerSecurityJwtConfiguration.java | 68 ++++++++ ...pcServerSecurityJwtInterceptorFactory.java | 49 ++++++ .../GrpcServerSecurityJwtInterceptor.java | 112 +++++++++++++ ...ecurityJwtConfigurationOverrideSpec.groovy | 24 +++ ...cServerSecurityJwtConfigurationSpec.groovy | 25 +++ ...erSecurityJwtInterceptorFactorySpec.groovy | 56 +++++++ ...rpcServerSecurityJwtInterceptorSpec.groovy | 153 ++++++++++++++++++ .../src/test/resources/logback.xml | 17 ++ settings.gradle | 1 + 13 files changed, 533 insertions(+), 6 deletions(-) create mode 100644 grpc-server-security-jwt/build.gradle create mode 100644 grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java create mode 100644 grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java create mode 100644 grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy create mode 100644 grpc-server-security-jwt/src/test/resources/logback.xml diff --git a/gradle.properties b/gradle.properties index 2bb3fda64..f2acfed06 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,9 @@ -projectVersion=2.3.1-SNAPSHOT +projectVersion=2.4.0.BUILD-SNAPSHOT micronautDocsVersion=1.0.24 micronautBuildVersion=1.1.5 -micronautVersion=2.3.1 +micronautDiscoveryClientVersion=2.0.1 +micronautSecurityVersion=2.3.0 +micronautVersion=2.3.2 micronautTestVersion=2.2.1 groovyVersion=3.0.4 spockVersion=2.0-M3-groovy-3.0 diff --git a/grpc-client-runtime/build.gradle b/grpc-client-runtime/build.gradle index 7be203990..7ea6e4d1c 100644 --- a/grpc-client-runtime/build.gradle +++ b/grpc-client-runtime/build.gradle @@ -12,11 +12,11 @@ dependencies { api "io.grpc:grpc-protobuf:$grpcVersion" api "io.grpc:grpc-stub:$grpcVersion" implementation "io.grpc:grpc-netty:$grpcVersion" - compileOnly "io.micronaut.discovery:micronaut-discovery-client:2.2.4" + compileOnly "io.micronaut.discovery:micronaut-discovery-client:$micronautDiscoveryClientVersion" compileOnly "io.micronaut:micronaut-tracing:$micronautVersion" compileOnly 'io.opentracing.contrib:opentracing-grpc:0.2.3' - testImplementation "io.micronaut.discovery:micronaut-discovery-client:2.2.4" + testImplementation "io.micronaut.discovery:micronaut-discovery-client:$micronautDiscoveryClientVersion" testImplementation 'io.opentracing:opentracing-mock:0.33.0' testImplementation "io.micronaut:micronaut-tracing:$micronautVersion" testImplementation 'io.opentracing.contrib:opentracing-grpc:0.2.3' diff --git a/grpc-server-runtime/build.gradle b/grpc-server-runtime/build.gradle index 470ddc3b5..23587241a 100644 --- a/grpc-server-runtime/build.gradle +++ b/grpc-server-runtime/build.gradle @@ -12,12 +12,12 @@ dependencies { api "io.grpc:grpc-protobuf:$grpcVersion" api "io.grpc:grpc-stub:$grpcVersion" - compileOnly "io.micronaut.discovery:micronaut-discovery-client:2.2.4" + compileOnly "io.micronaut.discovery:micronaut-discovery-client:$micronautDiscoveryClientVersion" compileOnly "io.micronaut:micronaut-tracing:$micronautVersion" compileOnly "io.micronaut:micronaut-management" compileOnly 'io.opentracing.contrib:opentracing-grpc:0.2.3' - testImplementation "io.micronaut.discovery:micronaut-discovery-client:2.2.4" + testImplementation "io.micronaut.discovery:micronaut-discovery-client:$micronautDiscoveryClientVersion" testImplementation 'io.opentracing:opentracing-mock:0.33.0' testImplementation "io.micronaut:micronaut-tracing:$micronautVersion" testImplementation 'io.opentracing.contrib:opentracing-grpc:0.2.3' diff --git a/grpc-server-security-jwt/build.gradle b/grpc-server-security-jwt/build.gradle new file mode 100644 index 000000000..9327a9b57 --- /dev/null +++ b/grpc-server-security-jwt/build.gradle @@ -0,0 +1,20 @@ + +dependencies { + + annotationProcessor "io.micronaut:micronaut-inject-java:$micronautVersion" + + api project(":grpc-server-runtime") + api "io.micronaut:micronaut-inject:$micronautVersion" + api "io.micronaut:micronaut-runtime:$micronautVersion" + api "com.nimbusds:nimbus-jose-jwt:9.4.2" + + implementation("io.micronaut.security:micronaut-security-jwt:$micronautSecurityVersion") { + exclude group: 'io.micronaut', module: 'micronaut-http' + exclude group: 'io.micronaut', module: 'micronaut-http-server' + } + + testImplementation "io.micronaut:micronaut-inject-groovy:$micronautVersion" + testImplementation "io.micronaut:micronaut-inject-java:$micronautVersion" + testImplementation 'io.micronaut.test:micronaut-test-spock:1.2.0' + +} \ No newline at end of file diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java new file mode 100644 index 000000000..7ee05a614 --- /dev/null +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.grpc.server.security.jwt; + +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.order.Ordered; +import io.micronaut.core.util.Toggleable; +import io.micronaut.grpc.server.GrpcServerConfiguration; + +import javax.validation.constraints.NotBlank; + +/** + * gRPC Security JWT configuration + * + * @since 2.4.0 + * @author Brian Wyka + */ +@ConfigurationProperties(GrpcServerSecurityJwtConfiguration.PREFIX) +@Requires(property = GrpcServerSecurityJwtConfiguration.PREFIX + ".enabled", value = "true", defaultValue = "false") +public interface GrpcServerSecurityJwtConfiguration extends Toggleable { + + String DEFAULT_METADATA_KEY_NAME = "JWT"; + String PREFIX = GrpcServerConfiguration.PREFIX + ".security.jwt"; + + /** + * Whether or not JWT server interceptor is enabled. Defaults to {@code false} if not configured. + * + * @return true if enabled, false otherwise + */ + @Override + @Bindable(defaultValue = "false") + boolean isEnabled(); + + /** + * The order to be applied to the server interceptor in the interceptor chain. Defaults + * to {@value io.micronaut.core.order.Ordered#HIGHEST_PRECEDENCE} if not configured. + * + * @return the order + */ + @Bindable(defaultValue = "" + Ordered.HIGHEST_PRECEDENCE) + int getOrder(); + + /** + * The name of the metadata key which holds the JWT. Defaults + * to {@value #DEFAULT_METADATA_KEY_NAME} if not configured. + * + * @return the metadata key name + */ + @NotBlank + @Bindable(defaultValue = DEFAULT_METADATA_KEY_NAME) + String getMetadataKeyName(); + +} diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java new file mode 100644 index 000000000..e164fead7 --- /dev/null +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java @@ -0,0 +1,49 @@ +package io.micronaut.grpc.server.security.jwt; + +import io.grpc.ServerInterceptor; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Requires; +import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor; +import io.micronaut.security.token.jwt.encryption.EncryptionConfiguration; +import io.micronaut.security.token.jwt.signature.SignatureConfiguration; +import io.micronaut.security.token.jwt.validator.GenericJwtClaimsValidator; +import io.micronaut.security.token.jwt.validator.JwtValidator; + +import javax.inject.Singleton; +import java.util.Collection; + +/** + * Factory for creating instances of gRPC server security JWT interceptors + * + * @since 2.4.0 + * @author Brian Wyka + */ +@Factory +@Requires(beans = GrpcServerSecurityJwtConfiguration.class) +public class GrpcServerSecurityJwtInterceptorFactory { + + /** + * Constructs an instance of {@link GrpcServerSecurityJwtInterceptor} based on configuration + * + * @param grpcServerSecurityJwtConfiguration the gRPC server security JWT configuration + * @param signatureConfigurations the signature configurations + * @param encryptionConfigurations the encryption configurations + * @param genericJwtClaimsValidators the generic JWT claims validators + * @return the server interceptor bean + */ + @Bean + @Singleton + public ServerInterceptor serverInterceptor(final GrpcServerSecurityJwtConfiguration grpcServerSecurityJwtConfiguration, + final Collection signatureConfigurations, + final Collection encryptionConfigurations, + final Collection genericJwtClaimsValidators) { + final JwtValidator jwtValidator = JwtValidator.builder() + .withSignatures(signatureConfigurations) + .withEncryptions(encryptionConfigurations) + .withClaimValidators(genericJwtClaimsValidators) + .build(); + return new GrpcServerSecurityJwtInterceptor(grpcServerSecurityJwtConfiguration, jwtValidator); + } + +} diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java new file mode 100644 index 000000000..6a29127d8 --- /dev/null +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.grpc.server.security.jwt.interceptor; + +import com.nimbusds.jwt.JWT; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.micronaut.core.order.Ordered; +import io.micronaut.grpc.server.security.jwt.GrpcServerSecurityJwtConfiguration; +import io.micronaut.security.token.jwt.validator.JwtValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + + +/** + * gRPC Server Security JWT Interceptor + * + * @since 2.4.0 + * @author Brian Wyka + */ +public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Ordered { + + private static final Logger LOG = LoggerFactory.getLogger(GrpcServerSecurityJwtInterceptor.class); + + private final Metadata.Key jwtMetadataKey; + private final JwtValidator jwtValidator; + private final int order; + + /** + * Create the interceptor based on the configuration. + * + * @param config the gRPC Security JWT configuration + * @param validator the JWT validator + */ + public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration config, final JwtValidator validator) { + jwtMetadataKey = Metadata.Key.of(config.getMetadataKeyName(), Metadata.ASCII_STRING_MARSHALLER); + jwtValidator = validator; + order = config.getOrder(); + } + + /** + * Intercept the call to validate the JSON web token. If the token is not present in the metadata, or + * if the token is not valid, this method will deny the request with a {@link StatusRuntimeException}. + * + * @param call the server call + * @param metadata the metadata + * @param next the next processor in the interceptor chain + * @param the type of the server request + * @param the type of the server response + * @throws StatusRuntimeException if token not present or invalid + */ + @Override + public ServerCall.Listener interceptCall(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { + if (!metadata.containsKey(jwtMetadataKey)) { + final String message = String.format("%s key missing in gRPC metadata", jwtMetadataKey.name()); + LOG.error(message); + throw Status.UNAUTHENTICATED.withDescription(message).asRuntimeException(); + } + final ServerCall.Listener listener = next.startCall(call, metadata); + final String jwt = metadata.get(jwtMetadataKey); + if (LOG.isDebugEnabled()) { + LOG.debug("JWT: {}", jwt); + } + final Optional jwtOptional = jwtValidator.validate(jwt, null); // We don't have an HttpRequest to send in here (hence null) + if (!jwtOptional.isPresent()) { + final String message = "JWT validation failed"; + LOG.error(message); + throw Status.PERMISSION_DENIED.withDescription(message).asRuntimeException(); + } + return new ForwardingServerCallListener.SimpleForwardingServerCallListener(listener) { }; + } + + /** + * Get the metadata key. + * + * @return the metadata key + */ + Metadata.Key getMetadataKey() { + return jwtMetadataKey; + } + + /** + * Get the order for this interceptor within the interceptor chain. + * + * @return the order + */ + @Override + public int getOrder() { + return order; + } + +} diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy new file mode 100644 index 000000000..16dceb6c1 --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy @@ -0,0 +1,24 @@ +package io.micronaut.grpc.server.security.jwt + +import io.micronaut.context.annotation.Property +import io.micronaut.core.order.Ordered +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification + +import javax.inject.Inject + +@MicronautTest +@Property(name = "grpc.server.security.jwt.enabled", value = "true") +class GrpcServerSecurityJwtConfigurationOverrideSpec extends Specification { + + @Inject + GrpcServerSecurityJwtConfiguration config + + def "GRPC server security JWT configuration defaults override"() { + expect: + config.enabled + config.metadataKeyName == "JWT" + config.order == Ordered.HIGHEST_PRECEDENCE + } + +} diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy new file mode 100644 index 000000000..15c20ab99 --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy @@ -0,0 +1,25 @@ +package io.micronaut.grpc.server.security.jwt + +import io.micronaut.context.annotation.Property +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification + +import javax.inject.Inject + +@MicronautTest +@Property(name = "grpc.server.security.jwt.enabled", value = "true") +@Property(name = "grpc.server.security.jwt.metadata-key-name", value = "AUTH") +@Property(name = "grpc.server.security.jwt.order", value = "100") +class GrpcServerSecurityJwtConfigurationSpec extends Specification { + + @Inject + GrpcServerSecurityJwtConfiguration config + + def "GRPC server security JWT configuration defaults override"() { + expect: + config.enabled + config.metadataKeyName == "AUTH" + config.order == 100 + } + +} diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy new file mode 100644 index 000000000..140e3464f --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy @@ -0,0 +1,56 @@ +package io.micronaut.grpc.server.security.jwt + +import io.grpc.ServerInterceptor +import io.micronaut.context.annotation.Property +import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification + +import javax.inject.Inject + +@MicronautTest +@Property(name = "grpc.server.security.jwt.enabled", value = "true") +@Property(name = "micronaut.security.token.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3t") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "false") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") +class GrpcServerSecurityJwtInterceptorFactorySpec extends Specification { + + @Inject + GrpcServerSecurityJwtInterceptor serverInterceptor + + def "serverInterceptor bean present"() { + expect: + serverInterceptor + } + + def "serverInterceptor"() { + given: + GrpcServerSecurityJwtConfiguration config = new GrpcServerSecurityJwtConfiguration() { + @Override + boolean isEnabled() { + return true + } + + @Override + int getOrder() { + return 0 + } + @Override + String getMetadataKeyName() { + return "JWT" + } + } + GrpcServerSecurityJwtInterceptorFactory factory = new GrpcServerSecurityJwtInterceptorFactory() + + when: + ServerInterceptor serverInterceptor = factory.serverInterceptor(config, [], [], []) + + then: + serverInterceptor + serverInterceptor instanceof GrpcServerSecurityJwtInterceptor + ((GrpcServerSecurityJwtInterceptor) serverInterceptor).order == config.order + } + +} diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy new file mode 100644 index 000000000..5fce7d1cf --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy @@ -0,0 +1,153 @@ +package io.micronaut.grpc.server.security.jwt.interceptor + + +import io.grpc.ForwardingServerCallListener +import io.grpc.Metadata +import io.grpc.ServerCall +import io.grpc.ServerCallHandler +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.micronaut.context.annotation.Property +import io.micronaut.security.authentication.UserDetails +import io.micronaut.security.token.jwt.generator.JwtTokenGenerator +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification + +import javax.inject.Inject + +@MicronautTest +@Property(name = "grpc.server.security.jwt.enabled", value = "true") +@Property(name = "grpc.server.security.jwt.metadata-key-name", value = METADATA_KEY_NAME) +@Property(name = "grpc.server.security.jwt.order", value = ORDER) +@Property(name = "micronaut.security.token.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") +class GrpcServerSecurityJwtInterceptorSpec extends Specification { + + static final String METADATA_KEY_NAME = "AUTH" + static final String ORDER = "10" + + @Inject + private JwtTokenGenerator jwtTokenGenerator + + @Inject + private GrpcServerSecurityJwtInterceptor interceptor + + def "test interceptor configured correctly"() { + expect: + interceptor.order == Integer.parseInt(ORDER) + interceptor.metadataKey == Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER) + } + + def "test interceptCall - missing JWT metadata key"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + ServerCallHandler mockServerCallHandler = Mock() + + when: + interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 0 * _ + + and: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.UNAUTHENTICATED.code + statusRuntimeException.status.description == "${METADATA_KEY_NAME.toLowerCase()} key missing in gRPC metadata" + } + + def "test interceptCall - invalid JWT"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + String jwt = "invalid-token" + metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + ServerCallHandler mockServerCallHandler = Mock() + ServerCall.Listener mockServerCallListener = Mock() + + when: + interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 0 * _ + + and: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.PERMISSION_DENIED.code + statusRuntimeException.status.description == "JWT validation failed" + } + + def "test interceptCall - invalid claims JWT"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + String jwt = jwtTokenGenerator.generateToken([:]).get() + metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + ServerCallHandler mockServerCallHandler = Mock() + ServerCall.Listener mockServerCallListener = Mock() + + when: + interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 0 * _ + + and: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.PERMISSION_DENIED.code + statusRuntimeException.status.description == "JWT validation failed" + } + + def "test interceptCall - expired JWT"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + UserDetails userDetails = new UserDetails("micronaut", []) + int expiration = 1 + String jwt = jwtTokenGenerator.generateToken(userDetails, 1).get() + metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + ServerCallHandler mockServerCallHandler = Mock() + ServerCall.Listener mockServerCallListener = Mock() + + when: + sleep((expiration * 1000) + 500) // Allow for token to expire + interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 0 * _ + + and: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.PERMISSION_DENIED.code + statusRuntimeException.status.description == "JWT validation failed" + } + + def "test interceptCall - valid JWT"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + UserDetails userDetails = new UserDetails("micronaut", ["admin"]) + String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() + metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + ServerCallHandler mockServerCallHandler = Mock() + ServerCall.Listener mockServerCallListener = Mock() + + when: + ServerCall.Listener serverCallListener = interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 0 * _ + + and: + serverCallListener + serverCallListener instanceof ForwardingServerCallListener.SimpleForwardingServerCallListener + } + +} diff --git a/grpc-server-security-jwt/src/test/resources/logback.xml b/grpc-server-security-jwt/src/test/resources/logback.xml new file mode 100644 index 000000000..4f5e804a2 --- /dev/null +++ b/grpc-server-security-jwt/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index a16b71ee9..88b6f44dc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,5 +3,6 @@ rootProject.name = 'grpc' include 'grpc-annotation' include 'grpc-client-runtime' include 'grpc-server-runtime' +include 'grpc-server-security-jwt' include 'grpc-runtime' include 'protobuff-support' \ No newline at end of file From 6fdb743bb2a5c2bd785c36bb5f7624346f9a08b5 Mon Sep 17 00:00:00 2001 From: brianwyka Date: Mon, 22 Feb 2021 13:39:08 -0500 Subject: [PATCH 02/12] Fix spotless checks --- .../GrpcServerSecurityJwtConfiguration.java | 3 ++- ...pcServerSecurityJwtInterceptorFactory.java | 19 +++++++++++++++++-- .../GrpcServerSecurityJwtInterceptor.java | 7 +++---- ...cServerSecurityJwtConfigurationSpec.groovy | 2 +- .../src/test/resources/logback.xml | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index 7ee05a614..1bfd66dda 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -25,7 +25,8 @@ import javax.validation.constraints.NotBlank; /** - * gRPC Security JWT configuration + * gRPC Security JWT configuration. + * * * @since 2.4.0 * @author Brian Wyka diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java index e164fead7..8383c212c 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.micronaut.grpc.server.security.jwt; import io.grpc.ServerInterceptor; @@ -14,7 +29,7 @@ import java.util.Collection; /** - * Factory for creating instances of gRPC server security JWT interceptors + * Factory for creating instances of gRPC server security JWT interceptors. * * @since 2.4.0 * @author Brian Wyka @@ -24,7 +39,7 @@ public class GrpcServerSecurityJwtInterceptorFactory { /** - * Constructs an instance of {@link GrpcServerSecurityJwtInterceptor} based on configuration + * Constructs an instance of {@link GrpcServerSecurityJwtInterceptor} based on configuration. * * @param grpcServerSecurityJwtConfiguration the gRPC server security JWT configuration * @param signatureConfigurations the signature configurations diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java index 6a29127d8..c7c4d79a1 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java @@ -22,7 +22,6 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; -import io.grpc.StatusRuntimeException; import io.micronaut.core.order.Ordered; import io.micronaut.grpc.server.security.jwt.GrpcServerSecurityJwtConfiguration; import io.micronaut.security.token.jwt.validator.JwtValidator; @@ -33,7 +32,7 @@ /** - * gRPC Server Security JWT Interceptor + * gRPC Server Security JWT Interceptor. * * @since 2.4.0 * @author Brian Wyka @@ -60,14 +59,14 @@ public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration /** * Intercept the call to validate the JSON web token. If the token is not present in the metadata, or - * if the token is not valid, this method will deny the request with a {@link StatusRuntimeException}. + * if the token is not valid, this method will deny the request with a {@link io.grpc.StatusRuntimeException}. * * @param call the server call * @param metadata the metadata * @param next the next processor in the interceptor chain * @param the type of the server request * @param the type of the server response - * @throws StatusRuntimeException if token not present or invalid + * @throws io.grpc.StatusRuntimeException if token not present or invalid */ @Override public ServerCall.Listener interceptCall(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy index 15c20ab99..4be44fef4 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy @@ -15,7 +15,7 @@ class GrpcServerSecurityJwtConfigurationSpec extends Specification { @Inject GrpcServerSecurityJwtConfiguration config - def "GRPC server security JWT configuration defaults override"() { + def "GRPC server security JWT configuration defaults"() { expect: config.enabled config.metadataKeyName == "AUTH" diff --git a/grpc-server-security-jwt/src/test/resources/logback.xml b/grpc-server-security-jwt/src/test/resources/logback.xml index 4f5e804a2..5aa46da93 100644 --- a/grpc-server-security-jwt/src/test/resources/logback.xml +++ b/grpc-server-security-jwt/src/test/resources/logback.xml @@ -12,6 +12,6 @@ - + \ No newline at end of file From f7a61b88c24214de8208cfff63566bf56f91fc2f Mon Sep 17 00:00:00 2001 From: brianwyka Date: Mon, 22 Feb 2021 20:51:11 -0500 Subject: [PATCH 03/12] Add more configurability and add documentation --- .../GrpcServerSecurityJwtConfiguration.java | 19 ++- .../GrpcServerSecurityJwtInterceptor.java | 10 +- ...ecurityJwtConfigurationDisabledSpec.groovy | 38 +++++ ...ecurityJwtConfigurationOverrideSpec.groovy | 12 +- ...cServerSecurityJwtConfigurationSpec.groovy | 10 +- ...erSecurityJwtInterceptorFactorySpec.groovy | 14 +- ...rSecurityJwtInterceptorOverrideSpec.groovy | 155 ++++++++++++++++++ ...rpcServerSecurityJwtInterceptorSpec.groovy | 21 +-- src/main/docs/guide/serverSecurity.adoc | 52 ++++++ src/main/docs/guide/toc.yml | 1 + 10 files changed, 304 insertions(+), 28 deletions(-) create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy create mode 100644 src/main/docs/guide/serverSecurity.adoc diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index 1bfd66dda..9e1edd055 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -15,6 +15,7 @@ */ package io.micronaut.grpc.server.security.jwt; +import io.grpc.Status; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Requires; import io.micronaut.core.bind.annotation.Bindable; @@ -54,7 +55,23 @@ public interface GrpcServerSecurityJwtConfiguration extends Toggleable { * @return the order */ @Bindable(defaultValue = "" + Ordered.HIGHEST_PRECEDENCE) - int getOrder(); + int getInterceptorOrder(); + + /** + * The {@link Status} returned by the interceptor when JWT is missing from metadata. + * + * @return the status + */ + @Bindable(defaultValue = "UNAUTHENTICATED") + Status.Code getMissingTokenStatus(); + + /** + * The {@link Status} returned by the interceptor when JWT validation fails. + * + * @return the status + */ + @Bindable(defaultValue = "PERMISSION_DENIED") + Status.Code getFailedValidationTokenStatus(); /** * The name of the metadata key which holds the JWT. Defaults diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java index c7c4d79a1..6cd2c1140 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java @@ -41,9 +41,9 @@ public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Orde private static final Logger LOG = LoggerFactory.getLogger(GrpcServerSecurityJwtInterceptor.class); + private final GrpcServerSecurityJwtConfiguration config; private final Metadata.Key jwtMetadataKey; private final JwtValidator jwtValidator; - private final int order; /** * Create the interceptor based on the configuration. @@ -52,9 +52,9 @@ public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Orde * @param validator the JWT validator */ public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration config, final JwtValidator validator) { + this.config = config; jwtMetadataKey = Metadata.Key.of(config.getMetadataKeyName(), Metadata.ASCII_STRING_MARSHALLER); jwtValidator = validator; - order = config.getOrder(); } /** @@ -73,7 +73,7 @@ public ServerCall.Listener interceptCall(final ServerCall call, if (!metadata.containsKey(jwtMetadataKey)) { final String message = String.format("%s key missing in gRPC metadata", jwtMetadataKey.name()); LOG.error(message); - throw Status.UNAUTHENTICATED.withDescription(message).asRuntimeException(); + throw Status.fromCode(config.getMissingTokenStatus()).withDescription(message).asRuntimeException(); } final ServerCall.Listener listener = next.startCall(call, metadata); final String jwt = metadata.get(jwtMetadataKey); @@ -84,7 +84,7 @@ public ServerCall.Listener interceptCall(final ServerCall call, if (!jwtOptional.isPresent()) { final String message = "JWT validation failed"; LOG.error(message); - throw Status.PERMISSION_DENIED.withDescription(message).asRuntimeException(); + throw Status.fromCode(config.getFailedValidationTokenStatus()).withDescription(message).asRuntimeException(); } return new ForwardingServerCallListener.SimpleForwardingServerCallListener(listener) { }; } @@ -105,7 +105,7 @@ Metadata.Key getMetadataKey() { */ @Override public int getOrder() { - return order; + return config.getInterceptorOrder(); } } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy new file mode 100644 index 000000000..c2572c79e --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy @@ -0,0 +1,38 @@ +package io.micronaut.grpc.server.security.jwt + +import io.micronaut.context.ApplicationContext +import io.micronaut.context.exceptions.NoSuchBeanException +import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification + +import javax.inject.Inject + + +@MicronautTest +class GrpcServerSecurityJwtConfigurationDisabledSpec extends Specification { + + @Inject + private ApplicationContext applicationContext + + def "beans are not loaded"() { + when: + applicationContext.getBean(GrpcServerSecurityJwtConfiguration) + + then: + thrown(NoSuchBeanException) + + when: + applicationContext.getBean(GrpcServerSecurityJwtInterceptorFactory) + + then: + thrown(NoSuchBeanException) + + when: + applicationContext.getBean(GrpcServerSecurityJwtInterceptor) + + then: + thrown(NoSuchBeanException) + } + +} \ No newline at end of file diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy index 16dceb6c1..880226398 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy @@ -1,7 +1,7 @@ package io.micronaut.grpc.server.security.jwt +import io.grpc.Status import io.micronaut.context.annotation.Property -import io.micronaut.core.order.Ordered import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification @@ -9,6 +9,10 @@ import javax.inject.Inject @MicronautTest @Property(name = "grpc.server.security.jwt.enabled", value = "true") +@Property(name = "grpc.server.security.jwt.metadata-key-name", value = "AUTH") +@Property(name = "grpc.server.security.jwt.missing-token-status", value = "NOT_FOUND") +@Property(name = "grpc.server.security.jwt.failed-validation-token-status", value = "ABORTED") +@Property(name = "grpc.server.security.jwt.interceptor-order", value = "100") class GrpcServerSecurityJwtConfigurationOverrideSpec extends Specification { @Inject @@ -17,8 +21,10 @@ class GrpcServerSecurityJwtConfigurationOverrideSpec extends Specification { def "GRPC server security JWT configuration defaults override"() { expect: config.enabled - config.metadataKeyName == "JWT" - config.order == Ordered.HIGHEST_PRECEDENCE + config.metadataKeyName == "AUTH" + config.missingTokenStatus == Status.NOT_FOUND.code + config.failedValidationTokenStatus == Status.ABORTED.code + config.interceptorOrder == 100 } } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy index 4be44fef4..d8370884b 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy @@ -1,6 +1,8 @@ package io.micronaut.grpc.server.security.jwt +import io.grpc.Status import io.micronaut.context.annotation.Property +import io.micronaut.core.order.Ordered import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification @@ -8,8 +10,6 @@ import javax.inject.Inject @MicronautTest @Property(name = "grpc.server.security.jwt.enabled", value = "true") -@Property(name = "grpc.server.security.jwt.metadata-key-name", value = "AUTH") -@Property(name = "grpc.server.security.jwt.order", value = "100") class GrpcServerSecurityJwtConfigurationSpec extends Specification { @Inject @@ -18,8 +18,10 @@ class GrpcServerSecurityJwtConfigurationSpec extends Specification { def "GRPC server security JWT configuration defaults"() { expect: config.enabled - config.metadataKeyName == "AUTH" - config.order == 100 + config.metadataKeyName == "JWT" + config.missingTokenStatus == Status.UNAUTHENTICATED.code + config.failedValidationTokenStatus == Status.PERMISSION_DENIED.code + config.interceptorOrder == Ordered.HIGHEST_PRECEDENCE } } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy index 140e3464f..0df66294a 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy @@ -1,6 +1,7 @@ package io.micronaut.grpc.server.security.jwt import io.grpc.ServerInterceptor +import io.grpc.Status import io.micronaut.context.annotation.Property import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor import io.micronaut.test.extensions.spock.annotation.MicronautTest @@ -32,12 +33,19 @@ class GrpcServerSecurityJwtInterceptorFactorySpec extends Specification { boolean isEnabled() { return true } - @Override - int getOrder() { + int getInterceptorOrder() { return 0 } @Override + Status.Code getMissingTokenStatus() { + return Status.UNAUTHENTICATED.code + } + @Override + Status.Code getFailedValidationTokenStatus() { + return Status.PERMISSION_DENIED.code + } + @Override String getMetadataKeyName() { return "JWT" } @@ -50,7 +58,7 @@ class GrpcServerSecurityJwtInterceptorFactorySpec extends Specification { then: serverInterceptor serverInterceptor instanceof GrpcServerSecurityJwtInterceptor - ((GrpcServerSecurityJwtInterceptor) serverInterceptor).order == config.order + ((GrpcServerSecurityJwtInterceptor) serverInterceptor).order == config.interceptorOrder } } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy new file mode 100644 index 000000000..d4075acd7 --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy @@ -0,0 +1,155 @@ +package io.micronaut.grpc.server.security.jwt.interceptor + + +import io.grpc.ForwardingServerCallListener +import io.grpc.Metadata +import io.grpc.ServerCall +import io.grpc.ServerCallHandler +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.micronaut.context.annotation.Property +import io.micronaut.security.authentication.UserDetails +import io.micronaut.security.token.jwt.generator.JwtTokenGenerator +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification + +import javax.inject.Inject + +@MicronautTest +@Property(name = "grpc.server.security.jwt.enabled", value = "true") +@Property(name = "grpc.server.security.jwt.metadata-key-name", value = METADATA_KEY_NAME) +@Property(name = "grpc.server.security.jwt.missing-token-status", value = "NOT_FOUND") +@Property(name = "grpc.server.security.jwt.failed-validation-token-status", value = "ABORTED") +@Property(name = "grpc.server.security.jwt.interceptor-order", value = ORDER) +@Property(name = "micronaut.security.token.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") +class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { + + static final String METADATA_KEY_NAME = "AUTH" + static final String ORDER = "10" + + @Inject + private JwtTokenGenerator jwtTokenGenerator + + @Inject + private GrpcServerSecurityJwtInterceptor interceptor + + def "test interceptor configured correctly"() { + expect: + interceptor.order == Integer.parseInt(ORDER) + interceptor.metadataKey == Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER) + } + + def "test interceptCall - missing JWT metadata key"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + ServerCallHandler mockServerCallHandler = Mock() + + when: + interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 0 * _ + + and: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.NOT_FOUND.code + statusRuntimeException.status.description == "${METADATA_KEY_NAME.toLowerCase()} key missing in gRPC metadata" + } + + def "test interceptCall - invalid JWT"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + String jwt = "invalid-token" + metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + ServerCallHandler mockServerCallHandler = Mock() + ServerCall.Listener mockServerCallListener = Mock() + + when: + interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 0 * _ + + and: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.ABORTED.code + statusRuntimeException.status.description == "JWT validation failed" + } + + def "test interceptCall - invalid claims JWT"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + String jwt = jwtTokenGenerator.generateToken([:]).get() + metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + ServerCallHandler mockServerCallHandler = Mock() + ServerCall.Listener mockServerCallListener = Mock() + + when: + interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 0 * _ + + and: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.ABORTED.code + statusRuntimeException.status.description == "JWT validation failed" + } + + def "test interceptCall - expired JWT"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + UserDetails userDetails = new UserDetails("micronaut", []) + int expiration = 1 + String jwt = jwtTokenGenerator.generateToken(userDetails, 1).get() + metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + ServerCallHandler mockServerCallHandler = Mock() + ServerCall.Listener mockServerCallListener = Mock() + + when: + sleep((expiration * 1000) + 500) // Allow for token to expire + interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 0 * _ + + and: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.ABORTED.code + statusRuntimeException.status.description == "JWT validation failed" + } + + def "test interceptCall - valid JWT"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + UserDetails userDetails = new UserDetails("micronaut", ["admin"]) + String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() + metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + ServerCallHandler mockServerCallHandler = Mock() + ServerCall.Listener mockServerCallListener = Mock() + + when: + ServerCall.Listener serverCallListener = interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 0 * _ + + and: + serverCallListener + serverCallListener instanceof ForwardingServerCallListener.SimpleForwardingServerCallListener + } + +} diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy index 5fce7d1cf..ac335e5b8 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy @@ -8,6 +8,8 @@ import io.grpc.ServerCallHandler import io.grpc.Status import io.grpc.StatusRuntimeException import io.micronaut.context.annotation.Property +import io.micronaut.core.order.Ordered +import io.micronaut.grpc.server.security.jwt.GrpcServerSecurityJwtConfiguration import io.micronaut.security.authentication.UserDetails import io.micronaut.security.token.jwt.generator.JwtTokenGenerator import io.micronaut.test.extensions.spock.annotation.MicronautTest @@ -17,8 +19,6 @@ import javax.inject.Inject @MicronautTest @Property(name = "grpc.server.security.jwt.enabled", value = "true") -@Property(name = "grpc.server.security.jwt.metadata-key-name", value = METADATA_KEY_NAME) -@Property(name = "grpc.server.security.jwt.order", value = ORDER) @Property(name = "micronaut.security.token.enabled", value = "true") @Property(name = "micronaut.security.token.jwt.enabled", value = "true") @Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") @@ -26,9 +26,6 @@ import javax.inject.Inject @Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") class GrpcServerSecurityJwtInterceptorSpec extends Specification { - static final String METADATA_KEY_NAME = "AUTH" - static final String ORDER = "10" - @Inject private JwtTokenGenerator jwtTokenGenerator @@ -37,8 +34,8 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { def "test interceptor configured correctly"() { expect: - interceptor.order == Integer.parseInt(ORDER) - interceptor.metadataKey == Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER) + interceptor.order == Ordered.HIGHEST_PRECEDENCE + interceptor.metadataKey == Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER) } def "test interceptCall - missing JWT metadata key"() { @@ -56,7 +53,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { and: StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) statusRuntimeException.status.code == Status.UNAUTHENTICATED.code - statusRuntimeException.status.description == "${METADATA_KEY_NAME.toLowerCase()} key missing in gRPC metadata" + statusRuntimeException.status.description == "${GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME.toLowerCase()} key missing in gRPC metadata" } def "test interceptCall - invalid JWT"() { @@ -64,7 +61,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { ServerCall mockServerCall = Mock() Metadata metadata = new Metadata() String jwt = "invalid-token" - metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() ServerCall.Listener mockServerCallListener = Mock() @@ -86,7 +83,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { ServerCall mockServerCall = Mock() Metadata metadata = new Metadata() String jwt = jwtTokenGenerator.generateToken([:]).get() - metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() ServerCall.Listener mockServerCallListener = Mock() @@ -110,7 +107,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { UserDetails userDetails = new UserDetails("micronaut", []) int expiration = 1 String jwt = jwtTokenGenerator.generateToken(userDetails, 1).get() - metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() ServerCall.Listener mockServerCallListener = Mock() @@ -134,7 +131,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { Metadata metadata = new Metadata() UserDetails userDetails = new UserDetails("micronaut", ["admin"]) String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() - metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) + metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() ServerCall.Listener mockServerCallListener = Mock() diff --git a/src/main/docs/guide/serverSecurity.adoc b/src/main/docs/guide/serverSecurity.adoc new file mode 100644 index 000000000..24fc24355 --- /dev/null +++ b/src/main/docs/guide/serverSecurity.adoc @@ -0,0 +1,52 @@ +=== gRPC Server Security Implementations +Currently, only `JWT` is supported for server-side security. + +==== gRPC Server Security JWT +JWT security is accomplished by an implementation of `io.grpc.ServerInterceptor`. + +If you are already using `io.micronaut.security:micronaut-security-jwt`, then configuration will be quite simple: + +===== Configuration + +.Enable JWT Validation +[source,yaml] +---- +grpc.server.security.jwt: + enabled: true # <1> +---- +<1> By default, this is disabled + +If you are not already using `io.micronaut.security:micronaut-security-jwt`, then you will also need to prepare +that configuration as well. Please read these https://micronaut-projects.github.io/micronaut-security/latest/guide/#jwt[docs] or this +https://guides.micronaut.io/micronaut-security-jwt/guide/index.html[guide]. + +The default configuration has the interceptor ordered with `@Ordered.HIGHEST_PRECEDENCE` and with +a metadata key name of `JWT`. See + +.Customizing JWT interceptor +[source,yaml] +---- +grpc.server.security.jwt: + metadata-key-name: AUTHORIZATION # <1> + interceptor-order: 100 # <2> +---- +<1> Use `AUTHORIZATION` as the metadata key name which holds the JSON web token for validation +<2> Set the interceptor order to `100` to control where it will be executed in the server interceptor chain + +===== Interceptor Behavior +The server interceptor will throw `io.grpc.StatusRuntimeException` when there is any issue with validating the `JWT`. + +1. **JWT is missing from metadata** - `io.grpc.Status.UNAUTHENTICATED` +2. **JWT provided but is invalid** - `io.grpc.Status.PERMISSION_DENIED` + +This can be changed by overriding the status configuration as demonstrated below: + +.Customizing gRPC Statuses +[source,yaml] +---- +grpc.server.security.jwt: + missing-token-status: NOT_FOUND # <1> + failed-validation-token-status: UNAUTHENTICATED # <2> +---- +<1> Return `io.grpc.Status.NOT_FOUND` when JWT is missing from metadata +<2> Return `io.grpc.Status.UNAUTHENTICATED` when JWT is provided but is invalid \ No newline at end of file diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 105309b9d..7f57d8ea3 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -3,6 +3,7 @@ introduction: releaseHistory: Release History gettingStarted: Getting Started server: gRPC Server +serverSecurity: gRPC Server Security client: gRPC Clients serviceDiscovery: Service Discovery tracing: Distributed Tracing From f344ddc7b4e857c892238ad5839986415ebbc625 Mon Sep 17 00:00:00 2001 From: brianwyka Date: Mon, 22 Feb 2021 23:41:17 -0500 Subject: [PATCH 04/12] Add support for intercept method patterns --- .../GrpcServerSecurityJwtConfiguration.java | 27 +++++++++-- .../GrpcServerSecurityJwtInterceptor.java | 13 ++++- ...ecurityJwtConfigurationOverrideSpec.groovy | 12 +++++ ...cServerSecurityJwtConfigurationSpec.groovy | 12 +++++ ...erSecurityJwtInterceptorFactorySpec.groovy | 4 ++ ...rSecurityJwtInterceptorOverrideSpec.groovy | 48 +++++++++++++++---- ...rpcServerSecurityJwtInterceptorSpec.groovy | 9 +--- src/main/docs/guide/serverSecurity.adoc | 21 +++++++- 8 files changed, 124 insertions(+), 22 deletions(-) diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index 9e1edd055..e689f2056 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -15,6 +15,7 @@ */ package io.micronaut.grpc.server.security.jwt; +import edu.umd.cs.findbugs.annotations.Nullable; import io.grpc.Status; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Requires; @@ -24,11 +25,11 @@ import io.micronaut.grpc.server.GrpcServerConfiguration; import javax.validation.constraints.NotBlank; +import java.util.Collection; /** * gRPC Security JWT configuration. * - * * @since 2.4.0 * @author Brian Wyka */ @@ -36,9 +37,16 @@ @Requires(property = GrpcServerSecurityJwtConfiguration.PREFIX + ".enabled", value = "true", defaultValue = "false") public interface GrpcServerSecurityJwtConfiguration extends Toggleable { - String DEFAULT_METADATA_KEY_NAME = "JWT"; + /** + * The configuration prefix + */ String PREFIX = GrpcServerConfiguration.PREFIX + ".security.jwt"; + /** + * The default name for the JWT metadata key + */ + String DEFAULT_METADATA_KEY_NAME = "JWT"; + /** * Whether or not JWT server interceptor is enabled. Defaults to {@code false} if not configured. * @@ -57,8 +65,20 @@ public interface GrpcServerSecurityJwtConfiguration extends Toggleable { @Bindable(defaultValue = "" + Ordered.HIGHEST_PRECEDENCE) int getInterceptorOrder(); + /** + * Get the list of fully qualified RPC method patterns which should be intercepted and interrogated for a valid JWT. + * If no values are provided, by default, all methods will be intercepted. + * + * @see io.grpc.MethodDescriptor#getFullMethodName() + * + * @return the intercept method names. + */ + @Nullable + Collection getInterceptMethodPatterns(); + /** * The {@link Status} returned by the interceptor when JWT is missing from metadata. + * The default value is {@link Status.Code#UNAUTHENTICATED} * * @return the status */ @@ -66,7 +86,8 @@ public interface GrpcServerSecurityJwtConfiguration extends Toggleable { Status.Code getMissingTokenStatus(); /** - * The {@link Status} returned by the interceptor when JWT validation fails. + * The {@link Status} returned by the interceptor when JWT validation fails. The + * default value is {@link Status.Code#PERMISSION_DENIED} * * @return the status */ diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java index 6cd2c1140..7cf464863 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java @@ -23,6 +23,7 @@ import io.grpc.ServerInterceptor; import io.grpc.Status; import io.micronaut.core.order.Ordered; +import io.micronaut.core.util.CollectionUtils; import io.micronaut.grpc.server.security.jwt.GrpcServerSecurityJwtConfiguration; import io.micronaut.security.token.jwt.validator.JwtValidator; import org.slf4j.Logger; @@ -70,12 +71,20 @@ public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration */ @Override public ServerCall.Listener interceptCall(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { + final boolean validateAll = CollectionUtils.isEmpty(config.getInterceptMethodPatterns()); + final boolean validateServiceMethod = validateAll || config.getInterceptMethodPatterns() + .stream() + .anyMatch(interceptMethodPattern -> call.getMethodDescriptor().getFullMethodName().matches(interceptMethodPattern)); + if (!validateServiceMethod) { + // Forward to the next server interceptor without validation + LOG.debug("JWT validation is skipped due to 'intercept-method-patterns' configuration"); + return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, metadata)) { }; + } if (!metadata.containsKey(jwtMetadataKey)) { final String message = String.format("%s key missing in gRPC metadata", jwtMetadataKey.name()); LOG.error(message); throw Status.fromCode(config.getMissingTokenStatus()).withDescription(message).asRuntimeException(); } - final ServerCall.Listener listener = next.startCall(call, metadata); final String jwt = metadata.get(jwtMetadataKey); if (LOG.isDebugEnabled()) { LOG.debug("JWT: {}", jwt); @@ -86,7 +95,7 @@ public ServerCall.Listener interceptCall(final ServerCall call, LOG.error(message); throw Status.fromCode(config.getFailedValidationTokenStatus()).withDescription(message).asRuntimeException(); } - return new ForwardingServerCallListener.SimpleForwardingServerCallListener(listener) { }; + return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, metadata)) { }; } /** diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy index 880226398..509b9df92 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy @@ -1,7 +1,9 @@ package io.micronaut.grpc.server.security.jwt +import io.grpc.ServerInterceptor import io.grpc.Status import io.micronaut.context.annotation.Property +import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification @@ -12,18 +14,28 @@ import javax.inject.Inject @Property(name = "grpc.server.security.jwt.metadata-key-name", value = "AUTH") @Property(name = "grpc.server.security.jwt.missing-token-status", value = "NOT_FOUND") @Property(name = "grpc.server.security.jwt.failed-validation-token-status", value = "ABORTED") +@Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/.*,example.Foo/getBar") @Property(name = "grpc.server.security.jwt.interceptor-order", value = "100") class GrpcServerSecurityJwtConfigurationOverrideSpec extends Specification { + @Inject + List serverInterceptors + @Inject GrpcServerSecurityJwtConfiguration config + def "server interceptor bean present"() { + expect: + serverInterceptors.find { it instanceof GrpcServerSecurityJwtInterceptor } + } + def "GRPC server security JWT configuration defaults override"() { expect: config.enabled config.metadataKeyName == "AUTH" config.missingTokenStatus == Status.NOT_FOUND.code config.failedValidationTokenStatus == Status.ABORTED.code + config.interceptMethodPatterns == ["example.Hello/.*", "example.Foo/getBar"] config.interceptorOrder == 100 } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy index d8370884b..5685745d2 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy @@ -1,8 +1,11 @@ package io.micronaut.grpc.server.security.jwt + +import io.grpc.ServerInterceptor import io.grpc.Status import io.micronaut.context.annotation.Property import io.micronaut.core.order.Ordered +import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification @@ -12,15 +15,24 @@ import javax.inject.Inject @Property(name = "grpc.server.security.jwt.enabled", value = "true") class GrpcServerSecurityJwtConfigurationSpec extends Specification { + @Inject + List serverInterceptors + @Inject GrpcServerSecurityJwtConfiguration config + def "server interceptor bean present"() { + expect: + serverInterceptors.find { it instanceof GrpcServerSecurityJwtInterceptor } + } + def "GRPC server security JWT configuration defaults"() { expect: config.enabled config.metadataKeyName == "JWT" config.missingTokenStatus == Status.UNAUTHENTICATED.code config.failedValidationTokenStatus == Status.PERMISSION_DENIED.code + !config.interceptMethodPatterns config.interceptorOrder == Ordered.HIGHEST_PRECEDENCE } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy index 0df66294a..62e775ea4 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy @@ -38,6 +38,10 @@ class GrpcServerSecurityJwtInterceptorFactorySpec extends Specification { return 0 } @Override + Collection getInterceptMethodPatterns() { + return null + } + @Override Status.Code getMissingTokenStatus() { return Status.UNAUTHENTICATED.code } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy index d4075acd7..6921c7cc7 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy @@ -3,6 +3,7 @@ package io.micronaut.grpc.server.security.jwt.interceptor import io.grpc.ForwardingServerCallListener import io.grpc.Metadata +import io.grpc.MethodDescriptor import io.grpc.ServerCall import io.grpc.ServerCallHandler import io.grpc.Status @@ -29,8 +30,16 @@ import javax.inject.Inject class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { static final String METADATA_KEY_NAME = "AUTH" + static final String INTERCEPT_FULL_METHOD_NAME = "example.Hello/GetWorld" static final String ORDER = "10" + private final MethodDescriptor methodDescriptor = MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName(INTERCEPT_FULL_METHOD_NAME) + .setRequestMarshaller(Mock(MethodDescriptor.Marshaller)) + .setResponseMarshaller(Mock(MethodDescriptor.Marshaller)) + .build() + @Inject private JwtTokenGenerator jwtTokenGenerator @@ -43,6 +52,27 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { interceptor.metadataKey == Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER) } + @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Foo/.*") + def "test interceptCall - intercept pattern not matched"() { + given: + ServerCall mockServerCall = Mock() + Metadata metadata = new Metadata() + ServerCallHandler mockServerCallHandler = Mock() + + when: + ServerCall.Listener serverCallListener = interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + + then: + 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> Mock(ServerCall.Listener) + 0 * _ + + and: + serverCallListener + serverCallListener instanceof ForwardingServerCallListener.SimpleForwardingServerCallListener + } + + @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/.*") def "test interceptCall - missing JWT metadata key"() { given: ServerCall mockServerCall = Mock() @@ -53,6 +83,7 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: + 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor 0 * _ and: @@ -61,6 +92,7 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { statusRuntimeException.status.description == "${METADATA_KEY_NAME.toLowerCase()} key missing in gRPC metadata" } + @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello.*") def "test interceptCall - invalid JWT"() { given: ServerCall mockServerCall = Mock() @@ -68,13 +100,12 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { String jwt = "invalid-token" metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() - ServerCall.Listener mockServerCallListener = Mock() when: interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor 0 * _ and: @@ -83,6 +114,7 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { statusRuntimeException.status.description == "JWT validation failed" } + @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/.*") def "test interceptCall - invalid claims JWT"() { given: ServerCall mockServerCall = Mock() @@ -90,13 +122,12 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { String jwt = jwtTokenGenerator.generateToken([:]).get() metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() - ServerCall.Listener mockServerCallListener = Mock() when: interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor 0 * _ and: @@ -105,6 +136,7 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { statusRuntimeException.status.description == "JWT validation failed" } + @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/Get.*") def "test interceptCall - expired JWT"() { given: ServerCall mockServerCall = Mock() @@ -114,14 +146,13 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { String jwt = jwtTokenGenerator.generateToken(userDetails, 1).get() metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() - ServerCall.Listener mockServerCallListener = Mock() when: sleep((expiration * 1000) + 500) // Allow for token to expire interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor 0 * _ and: @@ -130,6 +161,7 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { statusRuntimeException.status.description == "JWT validation failed" } + @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/GetWorld") def "test interceptCall - valid JWT"() { given: ServerCall mockServerCall = Mock() @@ -138,13 +170,13 @@ class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() - ServerCall.Listener mockServerCallListener = Mock() when: ServerCall.Listener serverCallListener = interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> Mock(ServerCall.Listener) 0 * _ and: diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy index ac335e5b8..c6b9076ef 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy @@ -63,13 +63,11 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { String jwt = "invalid-token" metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() - ServerCall.Listener mockServerCallListener = Mock() when: interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener 0 * _ and: @@ -85,13 +83,11 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { String jwt = jwtTokenGenerator.generateToken([:]).get() metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() - ServerCall.Listener mockServerCallListener = Mock() when: interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener 0 * _ and: @@ -109,14 +105,12 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { String jwt = jwtTokenGenerator.generateToken(userDetails, 1).get() metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() - ServerCall.Listener mockServerCallListener = Mock() when: sleep((expiration * 1000) + 500) // Allow for token to expire interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener 0 * _ and: @@ -133,13 +127,12 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) ServerCallHandler mockServerCallHandler = Mock() - ServerCall.Listener mockServerCallListener = Mock() when: ServerCall.Listener serverCallListener = interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> mockServerCallListener + 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> Mock( ServerCall.Listener) 0 * _ and: diff --git a/src/main/docs/guide/serverSecurity.adoc b/src/main/docs/guide/serverSecurity.adoc index 24fc24355..d22ca421a 100644 --- a/src/main/docs/guide/serverSecurity.adoc +++ b/src/main/docs/guide/serverSecurity.adoc @@ -20,8 +20,11 @@ If you are not already using `io.micronaut.security:micronaut-security-jwt`, the that configuration as well. Please read these https://micronaut-projects.github.io/micronaut-security/latest/guide/#jwt[docs] or this https://guides.micronaut.io/micronaut-security-jwt/guide/index.html[guide]. +===== Interceptor The default configuration has the interceptor ordered with `@Ordered.HIGHEST_PRECEDENCE` and with -a metadata key name of `JWT`. See +a metadata key name of `JWT`. + +This can be changed by overriding the interceptor configuration as demonstrated below: .Customizing JWT interceptor [source,yaml] @@ -33,6 +36,22 @@ grpc.server.security.jwt: <1> Use `AUTHORIZATION` as the metadata key name which holds the JSON web token for validation <2> Set the interceptor order to `100` to control where it will be executed in the server interceptor chain +===== Interceptor Patterns +By default, all gRPC services and methods will be intercepted. + +This can be changed by overriding the patterns configuration as demonstrated below: + +.Customizing Method Pattern Filters +[source,yaml] +---- +grpc.server.security.jwt: + intercept-method-patterns: + - app.ServiceName/MethodName # <1> + - app.OtherServiceName/.* # <2> +---- +<1> Matches an exact fully qualified gRPC Service Method Name - see https://grpc.github.io/grpc-java/javadoc/io/grpc/MethodDescriptor.html#getFullMethodName--[gRPC Javadocs]. +<2> Matches on a regular expression of the fully qualified gRPC service method name + ===== Interceptor Behavior The server interceptor will throw `io.grpc.StatusRuntimeException` when there is any issue with validating the `JWT`. From 8da6b580e5df747519be74b82c113606c68140a5 Mon Sep 17 00:00:00 2001 From: brianwyka Date: Mon, 22 Feb 2021 23:44:38 -0500 Subject: [PATCH 05/12] Checkstyle javadoc fix --- .../security/jwt/GrpcServerSecurityJwtConfiguration.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index e689f2056..88ea1afdb 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -38,12 +38,12 @@ public interface GrpcServerSecurityJwtConfiguration extends Toggleable { /** - * The configuration prefix + * The configuration prefix. */ String PREFIX = GrpcServerConfiguration.PREFIX + ".security.jwt"; /** - * The default name for the JWT metadata key + * The default name for the JWT metadata key. */ String DEFAULT_METADATA_KEY_NAME = "JWT"; From 9483b80f35ea1d49485bd555059e89f639e937f9 Mon Sep 17 00:00:00 2001 From: brianwyka Date: Tue, 23 Feb 2021 08:34:31 -0500 Subject: [PATCH 06/12] Add test protos and integration tests, fix gradle.properties SNAPSHOT --- gradle.properties | 2 +- grpc-server-security-jwt/build.gradle | 37 +++++- ...curityJwtInterceptorIntegrationSpec.groovy | 98 ++++++++++++++++ ...wtInterceptorSecuredIntegrationSpec.groovy | 110 ++++++++++++++++++ .../src/test/proto/helloworld.proto | 43 +++++++ 5 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorIntegrationSpec.groovy create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec.groovy create mode 100644 grpc-server-security-jwt/src/test/proto/helloworld.proto diff --git a/gradle.properties b/gradle.properties index f2acfed06..00e173042 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -projectVersion=2.4.0.BUILD-SNAPSHOT +projectVersion=2.4.0-SNAPSHOT micronautDocsVersion=1.0.24 micronautBuildVersion=1.1.5 micronautDiscoveryClientVersion=2.0.1 diff --git a/grpc-server-security-jwt/build.gradle b/grpc-server-security-jwt/build.gradle index 9327a9b57..fd05df0ce 100644 --- a/grpc-server-security-jwt/build.gradle +++ b/grpc-server-security-jwt/build.gradle @@ -1,7 +1,13 @@ +plugins { + id "net.ltgt.apt" version "0.21" + id 'com.google.protobuf' version '0.8.15' +} + dependencies { annotationProcessor "io.micronaut:micronaut-inject-java:$micronautVersion" + testAnnotationProcessor "io.micronaut.security:micronaut-security-annotations:$micronautSecurityVersion" api project(":grpc-server-runtime") api "io.micronaut:micronaut-inject:$micronautVersion" @@ -15,6 +21,35 @@ dependencies { testImplementation "io.micronaut:micronaut-inject-groovy:$micronautVersion" testImplementation "io.micronaut:micronaut-inject-java:$micronautVersion" - testImplementation 'io.micronaut.test:micronaut-test-spock:1.2.0' + testImplementation "io.micronaut.test:micronaut-test-spock:$micronautTestVersion" + +} + +protobuf { + protoc { artifact = "com.google.protobuf:protoc:$protocVersion" } + plugins { + grpc { artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } +} +sourceSets { + test { + java { + srcDirs 'build/generated/source/proto/test/grpc' + srcDirs 'build/generated/source/proto/test/java' + } + } +} + +protobuf { + protoc { artifact = "com.google.protobuf:protoc:$protocVersion" } + plugins { + grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.35.0" } + } + generateProtoTasks { + all()*.plugins { grpc {} } + } } \ No newline at end of file diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorIntegrationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorIntegrationSpec.groovy new file mode 100644 index 000000000..2970df094 --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorIntegrationSpec.groovy @@ -0,0 +1,98 @@ +package io.micronaut.grpc.server.security.jwt.interceptor + +import io.grpc.Channel +import io.grpc.Metadata +import io.grpc.StatusRuntimeException +import io.grpc.examples.helloworld.GreeterGrpc +import io.grpc.examples.helloworld.HelloReply +import io.grpc.examples.helloworld.HelloRequest +import io.grpc.stub.MetadataUtils +import io.grpc.stub.StreamObserver +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.grpc.annotation.GrpcChannel +import io.micronaut.grpc.server.GrpcServerChannel +import io.micronaut.security.authentication.UserDetails +import io.micronaut.security.token.jwt.generator.JwtTokenGenerator +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification + +import javax.inject.Inject +import javax.inject.Singleton + +@MicronautTest(environments = ["greeter-hello-world", Environment.TEST]) +@Property(name = "grpc.server.security.jwt.enabled", value = "true") +@Property(name = "micronaut.security.token.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") +class GrpcServerSecurityJwtInterceptorIntegrationSpec extends Specification { + + @Inject + private TestBean testBean + + def "test valid JWT works"() { + expect: + testBean.sayHelloWithJwt("Brian") == "Hello Brian" + } + + def "test missing JWT does not work"() { + when: + testBean.sayHelloWithoutJwt("Brian") + + then: + thrown(StatusRuntimeException) + } + + @Factory + @Requires(env = "greeter-hello-world") + static class Clients { + + @Singleton + GreeterGrpc.GreeterBlockingStub blockingStub(@GrpcChannel(GrpcServerChannel.NAME) Channel channel) { + GreeterGrpc.newBlockingStub(channel) + } + + } + + @Singleton + static class TestBean { + + @Inject + JwtTokenGenerator jwtTokenGenerator + + @Inject + GreeterGrpc.GreeterBlockingStub blockingStub + + String sayHelloWithJwt(String message) { + UserDetails userDetails = new UserDetails("micronaut", []) + String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() + Metadata metadata = new Metadata() + metadata.put(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER), jwt) + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + MetadataUtils.attachHeaders(blockingStub, metadata).sayHello(helloRequest).message + } + + String sayHelloWithoutJwt(String message) { + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + blockingStub.sayHello(helloRequest).message + } + + } + + @Singleton + static class GreeterImpl extends GreeterGrpc.GreeterImplBase { + + @Override + void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); + responseObserver.onNext(reply) + responseObserver.onCompleted() + } + + } + +} \ No newline at end of file diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec.groovy new file mode 100644 index 000000000..80b6e099d --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec.groovy @@ -0,0 +1,110 @@ +package io.micronaut.grpc.server.security.jwt.interceptor + +import io.grpc.Metadata +import io.grpc.StatusRuntimeException +import io.grpc.examples.helloworld.GreeterGrpc +import io.grpc.examples.helloworld.HelloReply +import io.grpc.examples.helloworld.HelloRequest +import io.grpc.stub.MetadataUtils +import io.grpc.stub.StreamObserver +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Property +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.grpc.annotation.GrpcChannel +import io.micronaut.grpc.server.GrpcServerChannel +import io.micronaut.security.annotation.Secured +import io.micronaut.security.authentication.UserDetails +import io.micronaut.security.token.jwt.generator.JwtTokenGenerator +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Ignore +import spock.lang.Specification + +import javax.inject.Inject +import javax.inject.Singleton +import java.nio.channels.Channel + +@Ignore // FIXME: need to get test working with @Secured annotation +@MicronautTest(environments = ["greeter-hello-world-secured", Environment.TEST]) +@Property(name = "grpc.server.security.jwt.enabled", value = "true") +@Property(name = "micronaut.security.token.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.enabled", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "true") +@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") +class GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec extends Specification { + + @Inject + private TestBean testBean + + def "test valid JWT with correct role works"() { + expect: + testBean.sayHelloWithJwt("Brian", "ROLE_ADMIN") == "Hello Brian" + } + + def "test valid JWT with incorrect role does not work"() { + when: + testBean.sayHelloWithJwt("Brian", "ROLE_USER") + + then: + thrown(StatusRuntimeException) + } + + def "test missing JWT does not work"() { + when: + testBean.sayHelloWithoutJwt("Brian") + + then: + thrown(StatusRuntimeException) + } + + @Factory + @Requires(env = "greeter-hello-world-secured") + static class Clients { + + @Singleton + GreeterGrpc.GreeterBlockingStub blockingStub(@GrpcChannel(GrpcServerChannel.NAME) Channel channel) { + GreeterGrpc.newBlockingStub(channel) + } + + } + + @Singleton + static class TestBean { + + @Inject + JwtTokenGenerator jwtTokenGenerator + + @Inject + GreeterGrpc.GreeterBlockingStub blockingStub + + String sayHelloWithJwt(String message, String role) { + UserDetails userDetails = new UserDetails("micronaut", [role]) + String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() + Metadata metadata = new Metadata() + metadata.put(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER), jwt) + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + MetadataUtils.attachHeaders(blockingStub, metadata).sayHello(helloRequest).message + } + + String sayHelloWithoutJwt(String message) { + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + blockingStub.sayHello(helloRequest).message + } + + } + + @Singleton + static class SecuredGreeterImpl extends GreeterGrpc.GreeterImplBase { + + @Override + @Secured("ROLE_ADMIN") + void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); + responseObserver.onNext(reply) + responseObserver.onCompleted() + } + + } + +} \ No newline at end of file diff --git a/grpc-server-security-jwt/src/test/proto/helloworld.proto b/grpc-server-security-jwt/src/test/proto/helloworld.proto new file mode 100644 index 000000000..38a3d17fe --- /dev/null +++ b/grpc-server-security-jwt/src/test/proto/helloworld.proto @@ -0,0 +1,43 @@ +// Copyright 2015 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The greeting service definition. +service Greeter2 { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} \ No newline at end of file From 3f1ea4005a87695941d775b249f48df15ca39625 Mon Sep 17 00:00:00 2001 From: brianwyka Date: Wed, 24 Feb 2021 11:26:28 -0500 Subject: [PATCH 07/12] Add role based security validation --- gradle.properties | 2 +- .../GrpcServerSecurityJwtConfiguration.java | 23 +- ...pcServerSecurityJwtInterceptorFactory.java | 10 +- .../GrpcServerSecurityJwtInterceptor.java | 148 ++++- ...ecurityJwtConfigurationDisabledSpec.groovy | 120 ++++- ...ecurityJwtConfigurationEnabledSpec.groovy} | 6 +- ...ecurityJwtConfigurationOverrideSpec.groovy | 14 +- ...erSecurityJwtInterceptorFactorySpec.groovy | 50 +- ...curityJwtInterceptorIntegrationSpec.groovy | 98 ---- ...rSecurityJwtInterceptorOverrideSpec.groovy | 187 ------- ...wtInterceptorSecuredIntegrationSpec.groovy | 110 ---- ...rpcServerSecurityJwtInterceptorSpec.groovy | 510 +++++++++++++++--- src/main/docs/guide/serverSecurity.adoc | 24 +- 13 files changed, 709 insertions(+), 593 deletions(-) rename grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/{GrpcServerSecurityJwtConfigurationSpec.groovy => GrpcServerSecurityJwtConfigurationEnabledSpec.groovy} (79%) delete mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorIntegrationSpec.groovy delete mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy delete mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec.groovy diff --git a/gradle.properties b/gradle.properties index 00e173042..2cc83a79d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ projectVersion=2.4.0-SNAPSHOT micronautDocsVersion=1.0.24 micronautBuildVersion=1.1.5 -micronautDiscoveryClientVersion=2.0.1 +micronautDiscoveryClientVersion=2.2.4 micronautSecurityVersion=2.3.0 micronautVersion=2.3.2 micronautTestVersion=2.2.1 diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index 88ea1afdb..b6635860b 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -21,8 +21,13 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.bind.annotation.Bindable; import io.micronaut.core.order.Ordered; +import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; import io.micronaut.grpc.server.GrpcServerConfiguration; +import io.micronaut.security.config.InterceptUrlMapPattern; +import io.micronaut.security.config.SecurityConfigurationProperties; +import io.micronaut.security.token.config.TokenConfigurationProperties; +import io.micronaut.security.token.jwt.config.JwtConfigurationProperties; import javax.validation.constraints.NotBlank; import java.util.Collection; @@ -34,28 +39,22 @@ * @author Brian Wyka */ @ConfigurationProperties(GrpcServerSecurityJwtConfiguration.PREFIX) -@Requires(property = GrpcServerSecurityJwtConfiguration.PREFIX + ".enabled", value = "true", defaultValue = "false") +@Requires(property = SecurityConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(property = TokenConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(property = JwtConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(property = GrpcServerSecurityJwtConfiguration.PREFIX + ".enabled", notEquals = StringUtils.FALSE) public interface GrpcServerSecurityJwtConfiguration extends Toggleable { /** * The configuration prefix. */ - String PREFIX = GrpcServerConfiguration.PREFIX + ".security.jwt"; + String PREFIX = GrpcServerConfiguration.PREFIX + ".security.token.jwt"; /** * The default name for the JWT metadata key. */ String DEFAULT_METADATA_KEY_NAME = "JWT"; - /** - * Whether or not JWT server interceptor is enabled. Defaults to {@code false} if not configured. - * - * @return true if enabled, false otherwise - */ - @Override - @Bindable(defaultValue = "false") - boolean isEnabled(); - /** * The order to be applied to the server interceptor in the interceptor chain. Defaults * to {@value io.micronaut.core.order.Ordered#HIGHEST_PRECEDENCE} if not configured. @@ -74,7 +73,7 @@ public interface GrpcServerSecurityJwtConfiguration extends Toggleable { * @return the intercept method names. */ @Nullable - Collection getInterceptMethodPatterns(); + Collection getInterceptMethodPatterns(); /** * The {@link Status} returned by the interceptor when JWT is missing from metadata. diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java index 8383c212c..5b9b7d358 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java @@ -20,6 +20,8 @@ import io.micronaut.context.annotation.Factory; import io.micronaut.context.annotation.Requires; import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor; +import io.micronaut.security.config.SecurityConfiguration; +import io.micronaut.security.token.RolesFinder; import io.micronaut.security.token.jwt.encryption.EncryptionConfiguration; import io.micronaut.security.token.jwt.signature.SignatureConfiguration; import io.micronaut.security.token.jwt.validator.GenericJwtClaimsValidator; @@ -45,6 +47,7 @@ public class GrpcServerSecurityJwtInterceptorFactory { * @param signatureConfigurations the signature configurations * @param encryptionConfigurations the encryption configurations * @param genericJwtClaimsValidators the generic JWT claims validators + * @param rolesFinder the roles finder for comparing roles with required roles * @return the server interceptor bean */ @Bean @@ -52,13 +55,16 @@ public class GrpcServerSecurityJwtInterceptorFactory { public ServerInterceptor serverInterceptor(final GrpcServerSecurityJwtConfiguration grpcServerSecurityJwtConfiguration, final Collection signatureConfigurations, final Collection encryptionConfigurations, - final Collection genericJwtClaimsValidators) { + final Collection genericJwtClaimsValidators, + final SecurityConfiguration securityConfiguration, + final RolesFinder rolesFinder) { final JwtValidator jwtValidator = JwtValidator.builder() .withSignatures(signatureConfigurations) .withEncryptions(encryptionConfigurations) .withClaimValidators(genericJwtClaimsValidators) .build(); - return new GrpcServerSecurityJwtInterceptor(grpcServerSecurityJwtConfiguration, jwtValidator); + final boolean rejectRolesNotFound = securityConfiguration.isRejectNotFound(); + return new GrpcServerSecurityJwtInterceptor(grpcServerSecurityJwtConfiguration, jwtValidator, rolesFinder, rejectRolesNotFound); } } diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java index 7cf464863..77c48b528 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java @@ -22,14 +22,24 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; +import io.grpc.StatusRuntimeException; import io.micronaut.core.order.Ordered; import io.micronaut.core.util.CollectionUtils; import io.micronaut.grpc.server.security.jwt.GrpcServerSecurityJwtConfiguration; +import io.micronaut.security.config.InterceptUrlMapPattern; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.security.token.RolesFinder; +import io.micronaut.security.token.jwt.generator.claims.JwtClaimsSetAdapter; import io.micronaut.security.token.jwt.validator.JwtValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.text.ParseException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; /** @@ -45,17 +55,27 @@ public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Orde private final GrpcServerSecurityJwtConfiguration config; private final Metadata.Key jwtMetadataKey; private final JwtValidator jwtValidator; + private final boolean rejectRolesNotFound; + private final RolesFinder rolesFinder; + /** * Create the interceptor based on the configuration. * * @param config the gRPC Security JWT configuration - * @param validator the JWT validator + * @param jwtValidator the JWT validator + * @param rolesFinder the roles finder + * @param rejectRolesNotFound whether or not to reject request if no roles found */ - public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration config, final JwtValidator validator) { + public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration config, + final JwtValidator jwtValidator, + final RolesFinder rolesFinder, + final boolean rejectRolesNotFound) { this.config = config; - jwtMetadataKey = Metadata.Key.of(config.getMetadataKeyName(), Metadata.ASCII_STRING_MARSHALLER); - jwtValidator = validator; + this.jwtMetadataKey = Metadata.Key.of(config.getMetadataKeyName(), Metadata.ASCII_STRING_MARSHALLER); + this.jwtValidator = jwtValidator; + this.rejectRolesNotFound = rejectRolesNotFound; + this.rolesFinder = rolesFinder; } /** @@ -71,40 +91,112 @@ public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration */ @Override public ServerCall.Listener interceptCall(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { - final boolean validateAll = CollectionUtils.isEmpty(config.getInterceptMethodPatterns()); - final boolean validateServiceMethod = validateAll || config.getInterceptMethodPatterns() - .stream() - .anyMatch(interceptMethodPattern -> call.getMethodDescriptor().getFullMethodName().matches(interceptMethodPattern)); - if (!validateServiceMethod) { - // Forward to the next server interceptor without validation - LOG.debug("JWT validation is skipped due to 'intercept-method-patterns' configuration"); - return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, metadata)) { }; + final List requiredAccesses = getRequiredAccesses(config, call); + if (CollectionUtils.isEmpty(config.getInterceptMethodPatterns()) && !rejectRolesNotFound) { + LOG.debug("JWT validation is skipped due to 'intercept-method-patterns' configuration being empty"); + return forward(call, metadata, next); + } else if (requiredAccesses.isEmpty() && rejectRolesNotFound) { + throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT validation failed since no roles were found and 'reject-not-found' = true"); + } else if (requiredAccesses.isEmpty()) { // We don't need tp validate JWT + LOG.debug("JWT validation is skipped due to no matching 'intercept-method-patterns'"); + return forward(call, metadata, next); + } else if (requiredAccesses.contains(SecurityRule.DENY_ALL)) { + throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT validation failed since denyAll() requirement met"); + } else if (requiredAccesses.contains(SecurityRule.IS_ANONYMOUS)) { // We don't need tp validate JWT + LOG.debug("JWT validation is skipped since isAnonymous() requirement met"); + return forward(call, metadata, next); } if (!metadata.containsKey(jwtMetadataKey)) { - final String message = String.format("%s key missing in gRPC metadata", jwtMetadataKey.name()); - LOG.error(message); - throw Status.fromCode(config.getMissingTokenStatus()).withDescription(message).asRuntimeException(); + throw statusRuntimeException(config.getMissingTokenStatus(), "JWT validation failed since no JWT was found in metadata"); } - final String jwt = metadata.get(jwtMetadataKey); - if (LOG.isDebugEnabled()) { - LOG.debug("JWT: {}", jwt); + final String token = metadata.get(jwtMetadataKey); + LOG.debug("JWT: {}", token); + final Optional jwtOptional = jwtValidator.validate(token, null); // We don't have an HttpRequest to send in here (hence null) + if (jwtOptional.isPresent()) { + if (requiredAccesses.contains(SecurityRule.IS_AUTHENTICATED)) { // Valid JWT is enough here + LOG.debug("JWT validation succeeded since isAuthenticated() requirement met"); + return forward(call, metadata, next); + } + return validateRoles(requiredAccesses, jwtOptional.get(), call, metadata, next); + } + throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT validation failed since no JWT was returned by validator"); + } + + /** + * Validate the JWT claims. + * + * @param requiredAccesses the list of required accesses + * @param jwt the JWT + * @param call the server call + * @param metadata the metadata + * @param next the next handler + * @param the type of the request + * @param the type of the response + * @return the server call listener + */ + private ServerCall.Listener validateRoles(final List requiredAccesses, final JWT jwt, + final ServerCall call, final Metadata metadata, final ServerCallHandler next) { + final List roles; + try { + roles = rolesFinder.findInClaims(new JwtClaimsSetAdapter(jwt.getJWTClaimsSet())); + } catch (final ParseException e) { + throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT validation failed due to parsing exception"); } - final Optional jwtOptional = jwtValidator.validate(jwt, null); // We don't have an HttpRequest to send in here (hence null) - if (!jwtOptional.isPresent()) { - final String message = "JWT validation failed"; - LOG.error(message); - throw Status.fromCode(config.getFailedValidationTokenStatus()).withDescription(message).asRuntimeException(); + if (rolesFinder.hasAnyRequiredRoles(requiredAccesses, roles)) { + LOG.debug("JWT validation succeeded with matching roles"); + return forward(call, metadata, next); + } else { + throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT does not contain required roles"); } - return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, metadata)) { }; } /** - * Get the metadata key. + * Create a {@link StatusRuntimeException} for the given status code and message and log an error. * - * @return the metadata key + * @param statusCode the status code + * @param message the message to log an error with + * @return the status runtime exception */ - Metadata.Key getMetadataKey() { - return jwtMetadataKey; + private static StatusRuntimeException statusRuntimeException(final Status.Code statusCode, final String message) { + LOG.error(message); + return Status.fromCode(statusCode).withDescription("JWT validation failed").asRuntimeException(); + } + + /** + * Get the required access for the server call. + * + * TODO: Is there any way we can get access to values from @Secured annotation here ?? + * + * @param config the configuration + * @param serverCall the server call + * @param the request type + * @param the response type + * @return the required access + */ + private static List getRequiredAccesses(final GrpcServerSecurityJwtConfiguration config, final ServerCall serverCall) { + if (CollectionUtils.isEmpty(config.getInterceptMethodPatterns())) { + return Collections.emptyList(); + } + return config.getInterceptMethodPatterns() + .stream() + .filter(interceptMethodPattern -> serverCall.getMethodDescriptor().getFullMethodName().matches(interceptMethodPattern.getPattern())) + .map(InterceptUrlMapPattern::getAccess) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + /** + * Forward the call to next handler. + * + * @param call the server call + * @param metadata the metadata + * @param next the next handler + * @param the type of the request + * @param the type of the response + * @return the server call listener + */ + private static ServerCall.Listener forward(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { + return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, metadata)) { }; } /** diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy index c2572c79e..d82e41951 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy @@ -3,36 +3,136 @@ package io.micronaut.grpc.server.security.jwt import io.micronaut.context.ApplicationContext import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor -import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification -import javax.inject.Inject +class GrpcServerSecurityJwtConfigurationDisabledSpec extends Specification { + def "beans are not loaded when security not enabled"() { + given: + def config = [ + "micronaut.security.enabled": false, + "micronaut.security.token.enabled": true, + "micronaut.security.token.jwt.enabled": true, + "grpc.server.security.token.jwt.enabled": true + ] + def context = ApplicationContext.run(config) -@MicronautTest -class GrpcServerSecurityJwtConfigurationDisabledSpec extends Specification { + when: + context.getBean(GrpcServerSecurityJwtConfiguration) + + then: + thrown(NoSuchBeanException) + + when: + context.getBean(GrpcServerSecurityJwtInterceptorFactory) + + then: + thrown(NoSuchBeanException) + + when: + context.getBean(GrpcServerSecurityJwtInterceptor) + + then: + thrown(NoSuchBeanException) + + cleanup: + context.close() + } + + def "beans are not loaded when security token not enabled"() { + given: + def config = [ + "micronaut.security.enabled": true, + "micronaut.security.token.enabled": false, + "micronaut.security.token.jwt.enabled": true, + "grpc.server.security.token.jwt.enabled": true + ] + def context = ApplicationContext.run(config) + + when: + context.getBean(GrpcServerSecurityJwtConfiguration) - @Inject - private ApplicationContext applicationContext + then: + thrown(NoSuchBeanException) - def "beans are not loaded"() { when: - applicationContext.getBean(GrpcServerSecurityJwtConfiguration) + context.getBean(GrpcServerSecurityJwtInterceptorFactory) then: thrown(NoSuchBeanException) when: - applicationContext.getBean(GrpcServerSecurityJwtInterceptorFactory) + context.getBean(GrpcServerSecurityJwtInterceptor) then: thrown(NoSuchBeanException) + cleanup: + context.close() + } + + def "beans are not loaded when security token jwt not enabled"() { + given: + def config = [ + "micronaut.security.enabled": true, + "micronaut.security.token.enabled": true, + "micronaut.security.token.jwt.enabled": false, + "grpc.server.security.token.jwt.enabled": true + ] + def context = ApplicationContext.run(config) + when: - applicationContext.getBean(GrpcServerSecurityJwtInterceptor) + context.getBean(GrpcServerSecurityJwtConfiguration) then: thrown(NoSuchBeanException) + + when: + context.getBean(GrpcServerSecurityJwtInterceptorFactory) + + then: + thrown(NoSuchBeanException) + + when: + context.getBean(GrpcServerSecurityJwtInterceptor) + + then: + thrown(NoSuchBeanException) + + cleanup: + context.close() + } + + def "beans are not loaded when grpc security jwt not enabled"() { + given: + def config = [ + "micronaut.security.enabled": true, + "micronaut.security.token.enabled": true, + "micronaut.security.token.jwt.enabled": true, + "grpc.server.security.token.jwt.enabled": false + ] + def context = ApplicationContext.run(config) + + when: + context.getBean(GrpcServerSecurityJwtConfiguration) + + then: + thrown(NoSuchBeanException) + + when: + context.getBean(GrpcServerSecurityJwtInterceptorFactory) + + then: + thrown(NoSuchBeanException) + + when: + context.getBean(GrpcServerSecurityJwtInterceptor) + + then: + thrown(NoSuchBeanException) + + cleanup: + context.close() } } \ No newline at end of file diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy similarity index 79% rename from grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy rename to grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy index 5685745d2..fe02c1316 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy @@ -12,8 +12,10 @@ import spock.lang.Specification import javax.inject.Inject @MicronautTest -@Property(name = "grpc.server.security.jwt.enabled", value = "true") -class GrpcServerSecurityJwtConfigurationSpec extends Specification { +@Property(name = "micronaut.security.enabled", value = "true") +@Property(name = "micronaut.security.token.enabled", value = "true") +@Property(name = "grpc.server.security.token.jwt.enabled", value = "true") +class GrpcServerSecurityJwtConfigurationEnabledSpec extends Specification { @Inject List serverInterceptors diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy index 509b9df92..240f7379d 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy @@ -10,12 +10,13 @@ import spock.lang.Specification import javax.inject.Inject @MicronautTest -@Property(name = "grpc.server.security.jwt.enabled", value = "true") -@Property(name = "grpc.server.security.jwt.metadata-key-name", value = "AUTH") -@Property(name = "grpc.server.security.jwt.missing-token-status", value = "NOT_FOUND") -@Property(name = "grpc.server.security.jwt.failed-validation-token-status", value = "ABORTED") -@Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/.*,example.Foo/getBar") -@Property(name = "grpc.server.security.jwt.interceptor-order", value = "100") +@Property(name = "micronaut.security.enabled", value = "true") +@Property(name = "micronaut.security.token.enabled", value = "true") +@Property(name = "grpc.server.security.token.jwt.enabled", value = "true") +@Property(name = "grpc.server.security.token.jwt.metadata-key-name", value = "AUTH") +@Property(name = "grpc.server.security.token.jwt.missing-token-status", value = "NOT_FOUND") +@Property(name = "grpc.server.security.token.jwt.failed-validation-token-status", value = "ABORTED") +@Property(name = "grpc.server.security.token.jwt.interceptor-order", value = "100") class GrpcServerSecurityJwtConfigurationOverrideSpec extends Specification { @Inject @@ -35,7 +36,6 @@ class GrpcServerSecurityJwtConfigurationOverrideSpec extends Specification { config.metadataKeyName == "AUTH" config.missingTokenStatus == Status.NOT_FOUND.code config.failedValidationTokenStatus == Status.ABORTED.code - config.interceptMethodPatterns == ["example.Hello/.*", "example.Foo/getBar"] config.interceptorOrder == 100 } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy index 62e775ea4..100801d94 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy @@ -1,7 +1,6 @@ package io.micronaut.grpc.server.security.jwt -import io.grpc.ServerInterceptor -import io.grpc.Status +import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Property import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor import io.micronaut.test.extensions.spock.annotation.MicronautTest @@ -10,7 +9,9 @@ import spock.lang.Specification import javax.inject.Inject @MicronautTest -@Property(name = "grpc.server.security.jwt.enabled", value = "true") +@Property(name = "micronaut.security.enabled", value = "true") +@Property(name = "micronaut.security.token.enabled", value = "true") +@Property(name = "grpc.server.security.token.jwt.enabled", value = "true") @Property(name = "micronaut.security.token.enabled", value = "true") @Property(name = "micronaut.security.token.jwt.enabled", value = "true") @Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3t") @@ -19,50 +20,11 @@ import javax.inject.Inject class GrpcServerSecurityJwtInterceptorFactorySpec extends Specification { @Inject - GrpcServerSecurityJwtInterceptor serverInterceptor + ApplicationContext applicationContext def "serverInterceptor bean present"() { expect: - serverInterceptor - } - - def "serverInterceptor"() { - given: - GrpcServerSecurityJwtConfiguration config = new GrpcServerSecurityJwtConfiguration() { - @Override - boolean isEnabled() { - return true - } - @Override - int getInterceptorOrder() { - return 0 - } - @Override - Collection getInterceptMethodPatterns() { - return null - } - @Override - Status.Code getMissingTokenStatus() { - return Status.UNAUTHENTICATED.code - } - @Override - Status.Code getFailedValidationTokenStatus() { - return Status.PERMISSION_DENIED.code - } - @Override - String getMetadataKeyName() { - return "JWT" - } - } - GrpcServerSecurityJwtInterceptorFactory factory = new GrpcServerSecurityJwtInterceptorFactory() - - when: - ServerInterceptor serverInterceptor = factory.serverInterceptor(config, [], [], []) - - then: - serverInterceptor - serverInterceptor instanceof GrpcServerSecurityJwtInterceptor - ((GrpcServerSecurityJwtInterceptor) serverInterceptor).order == config.interceptorOrder + applicationContext.getBean(GrpcServerSecurityJwtInterceptor) } } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorIntegrationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorIntegrationSpec.groovy deleted file mode 100644 index 2970df094..000000000 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorIntegrationSpec.groovy +++ /dev/null @@ -1,98 +0,0 @@ -package io.micronaut.grpc.server.security.jwt.interceptor - -import io.grpc.Channel -import io.grpc.Metadata -import io.grpc.StatusRuntimeException -import io.grpc.examples.helloworld.GreeterGrpc -import io.grpc.examples.helloworld.HelloReply -import io.grpc.examples.helloworld.HelloRequest -import io.grpc.stub.MetadataUtils -import io.grpc.stub.StreamObserver -import io.micronaut.context.annotation.Factory -import io.micronaut.context.annotation.Property -import io.micronaut.context.annotation.Requires -import io.micronaut.context.env.Environment -import io.micronaut.grpc.annotation.GrpcChannel -import io.micronaut.grpc.server.GrpcServerChannel -import io.micronaut.security.authentication.UserDetails -import io.micronaut.security.token.jwt.generator.JwtTokenGenerator -import io.micronaut.test.extensions.spock.annotation.MicronautTest -import spock.lang.Specification - -import javax.inject.Inject -import javax.inject.Singleton - -@MicronautTest(environments = ["greeter-hello-world", Environment.TEST]) -@Property(name = "grpc.server.security.jwt.enabled", value = "true") -@Property(name = "micronaut.security.token.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "true") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") -class GrpcServerSecurityJwtInterceptorIntegrationSpec extends Specification { - - @Inject - private TestBean testBean - - def "test valid JWT works"() { - expect: - testBean.sayHelloWithJwt("Brian") == "Hello Brian" - } - - def "test missing JWT does not work"() { - when: - testBean.sayHelloWithoutJwt("Brian") - - then: - thrown(StatusRuntimeException) - } - - @Factory - @Requires(env = "greeter-hello-world") - static class Clients { - - @Singleton - GreeterGrpc.GreeterBlockingStub blockingStub(@GrpcChannel(GrpcServerChannel.NAME) Channel channel) { - GreeterGrpc.newBlockingStub(channel) - } - - } - - @Singleton - static class TestBean { - - @Inject - JwtTokenGenerator jwtTokenGenerator - - @Inject - GreeterGrpc.GreeterBlockingStub blockingStub - - String sayHelloWithJwt(String message) { - UserDetails userDetails = new UserDetails("micronaut", []) - String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() - Metadata metadata = new Metadata() - metadata.put(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER), jwt) - HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() - MetadataUtils.attachHeaders(blockingStub, metadata).sayHello(helloRequest).message - } - - String sayHelloWithoutJwt(String message) { - HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() - blockingStub.sayHello(helloRequest).message - } - - } - - @Singleton - static class GreeterImpl extends GreeterGrpc.GreeterImplBase { - - @Override - void sayHello(HelloRequest request, StreamObserver responseObserver) { - HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); - responseObserver.onNext(reply) - responseObserver.onCompleted() - } - - } - -} \ No newline at end of file diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy deleted file mode 100644 index 6921c7cc7..000000000 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorOverrideSpec.groovy +++ /dev/null @@ -1,187 +0,0 @@ -package io.micronaut.grpc.server.security.jwt.interceptor - - -import io.grpc.ForwardingServerCallListener -import io.grpc.Metadata -import io.grpc.MethodDescriptor -import io.grpc.ServerCall -import io.grpc.ServerCallHandler -import io.grpc.Status -import io.grpc.StatusRuntimeException -import io.micronaut.context.annotation.Property -import io.micronaut.security.authentication.UserDetails -import io.micronaut.security.token.jwt.generator.JwtTokenGenerator -import io.micronaut.test.extensions.spock.annotation.MicronautTest -import spock.lang.Specification - -import javax.inject.Inject - -@MicronautTest -@Property(name = "grpc.server.security.jwt.enabled", value = "true") -@Property(name = "grpc.server.security.jwt.metadata-key-name", value = METADATA_KEY_NAME) -@Property(name = "grpc.server.security.jwt.missing-token-status", value = "NOT_FOUND") -@Property(name = "grpc.server.security.jwt.failed-validation-token-status", value = "ABORTED") -@Property(name = "grpc.server.security.jwt.interceptor-order", value = ORDER) -@Property(name = "micronaut.security.token.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "true") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") -class GrpcServerSecurityJwtInterceptorOverrideSpec extends Specification { - - static final String METADATA_KEY_NAME = "AUTH" - static final String INTERCEPT_FULL_METHOD_NAME = "example.Hello/GetWorld" - static final String ORDER = "10" - - private final MethodDescriptor methodDescriptor = MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName(INTERCEPT_FULL_METHOD_NAME) - .setRequestMarshaller(Mock(MethodDescriptor.Marshaller)) - .setResponseMarshaller(Mock(MethodDescriptor.Marshaller)) - .build() - - @Inject - private JwtTokenGenerator jwtTokenGenerator - - @Inject - private GrpcServerSecurityJwtInterceptor interceptor - - def "test interceptor configured correctly"() { - expect: - interceptor.order == Integer.parseInt(ORDER) - interceptor.metadataKey == Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER) - } - - @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Foo/.*") - def "test interceptCall - intercept pattern not matched"() { - given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - ServerCallHandler mockServerCallHandler = Mock() - - when: - ServerCall.Listener serverCallListener = interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) - - then: - 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> Mock(ServerCall.Listener) - 0 * _ - - and: - serverCallListener - serverCallListener instanceof ForwardingServerCallListener.SimpleForwardingServerCallListener - } - - @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/.*") - def "test interceptCall - missing JWT metadata key"() { - given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - ServerCallHandler mockServerCallHandler = Mock() - - when: - interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) - - then: - 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor - 0 * _ - - and: - StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) - statusRuntimeException.status.code == Status.NOT_FOUND.code - statusRuntimeException.status.description == "${METADATA_KEY_NAME.toLowerCase()} key missing in gRPC metadata" - } - - @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello.*") - def "test interceptCall - invalid JWT"() { - given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - String jwt = "invalid-token" - metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) - ServerCallHandler mockServerCallHandler = Mock() - - when: - interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) - - then: - 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor - 0 * _ - - and: - StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) - statusRuntimeException.status.code == Status.ABORTED.code - statusRuntimeException.status.description == "JWT validation failed" - } - - @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/.*") - def "test interceptCall - invalid claims JWT"() { - given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - String jwt = jwtTokenGenerator.generateToken([:]).get() - metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) - ServerCallHandler mockServerCallHandler = Mock() - - when: - interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) - - then: - 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor - 0 * _ - - and: - StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) - statusRuntimeException.status.code == Status.ABORTED.code - statusRuntimeException.status.description == "JWT validation failed" - } - - @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/Get.*") - def "test interceptCall - expired JWT"() { - given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - UserDetails userDetails = new UserDetails("micronaut", []) - int expiration = 1 - String jwt = jwtTokenGenerator.generateToken(userDetails, 1).get() - metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) - ServerCallHandler mockServerCallHandler = Mock() - - when: - sleep((expiration * 1000) + 500) // Allow for token to expire - interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) - - then: - 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor - 0 * _ - - and: - StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) - statusRuntimeException.status.code == Status.ABORTED.code - statusRuntimeException.status.description == "JWT validation failed" - } - - @Property(name = "grpc.server.security.jwt.intercept-method-patterns", value = "example.Hello/GetWorld") - def "test interceptCall - valid JWT"() { - given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - UserDetails userDetails = new UserDetails("micronaut", ["admin"]) - String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() - metadata.put(Metadata.Key.of(METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) - ServerCallHandler mockServerCallHandler = Mock() - - when: - ServerCall.Listener serverCallListener = interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) - - then: - 1 * mockServerCall.getMethodDescriptor() >> methodDescriptor - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> Mock(ServerCall.Listener) - 0 * _ - - and: - serverCallListener - serverCallListener instanceof ForwardingServerCallListener.SimpleForwardingServerCallListener - } - -} diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec.groovy deleted file mode 100644 index 80b6e099d..000000000 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec.groovy +++ /dev/null @@ -1,110 +0,0 @@ -package io.micronaut.grpc.server.security.jwt.interceptor - -import io.grpc.Metadata -import io.grpc.StatusRuntimeException -import io.grpc.examples.helloworld.GreeterGrpc -import io.grpc.examples.helloworld.HelloReply -import io.grpc.examples.helloworld.HelloRequest -import io.grpc.stub.MetadataUtils -import io.grpc.stub.StreamObserver -import io.micronaut.context.annotation.Factory -import io.micronaut.context.annotation.Property -import io.micronaut.context.annotation.Requires -import io.micronaut.context.env.Environment -import io.micronaut.grpc.annotation.GrpcChannel -import io.micronaut.grpc.server.GrpcServerChannel -import io.micronaut.security.annotation.Secured -import io.micronaut.security.authentication.UserDetails -import io.micronaut.security.token.jwt.generator.JwtTokenGenerator -import io.micronaut.test.extensions.spock.annotation.MicronautTest -import spock.lang.Ignore -import spock.lang.Specification - -import javax.inject.Inject -import javax.inject.Singleton -import java.nio.channels.Channel - -@Ignore // FIXME: need to get test working with @Secured annotation -@MicronautTest(environments = ["greeter-hello-world-secured", Environment.TEST]) -@Property(name = "grpc.server.security.jwt.enabled", value = "true") -@Property(name = "micronaut.security.token.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "true") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") -class GrpcServerSecurityJwtInterceptorSecuredIntegrationSpec extends Specification { - - @Inject - private TestBean testBean - - def "test valid JWT with correct role works"() { - expect: - testBean.sayHelloWithJwt("Brian", "ROLE_ADMIN") == "Hello Brian" - } - - def "test valid JWT with incorrect role does not work"() { - when: - testBean.sayHelloWithJwt("Brian", "ROLE_USER") - - then: - thrown(StatusRuntimeException) - } - - def "test missing JWT does not work"() { - when: - testBean.sayHelloWithoutJwt("Brian") - - then: - thrown(StatusRuntimeException) - } - - @Factory - @Requires(env = "greeter-hello-world-secured") - static class Clients { - - @Singleton - GreeterGrpc.GreeterBlockingStub blockingStub(@GrpcChannel(GrpcServerChannel.NAME) Channel channel) { - GreeterGrpc.newBlockingStub(channel) - } - - } - - @Singleton - static class TestBean { - - @Inject - JwtTokenGenerator jwtTokenGenerator - - @Inject - GreeterGrpc.GreeterBlockingStub blockingStub - - String sayHelloWithJwt(String message, String role) { - UserDetails userDetails = new UserDetails("micronaut", [role]) - String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() - Metadata metadata = new Metadata() - metadata.put(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER), jwt) - HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() - MetadataUtils.attachHeaders(blockingStub, metadata).sayHello(helloRequest).message - } - - String sayHelloWithoutJwt(String message) { - HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() - blockingStub.sayHello(helloRequest).message - } - - } - - @Singleton - static class SecuredGreeterImpl extends GreeterGrpc.GreeterImplBase { - - @Override - @Secured("ROLE_ADMIN") - void sayHello(HelloRequest request, StreamObserver responseObserver) { - HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); - responseObserver.onNext(reply) - responseObserver.onCompleted() - } - - } - -} \ No newline at end of file diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy index c6b9076ef..873a56740 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy @@ -1,143 +1,489 @@ package io.micronaut.grpc.server.security.jwt.interceptor - -import io.grpc.ForwardingServerCallListener +import io.grpc.Channel import io.grpc.Metadata -import io.grpc.ServerCall -import io.grpc.ServerCallHandler import io.grpc.Status import io.grpc.StatusRuntimeException -import io.micronaut.context.annotation.Property -import io.micronaut.core.order.Ordered -import io.micronaut.grpc.server.security.jwt.GrpcServerSecurityJwtConfiguration +import io.grpc.examples.helloworld.GreeterGrpc +import io.grpc.examples.helloworld.HelloReply +import io.grpc.examples.helloworld.HelloRequest +import io.grpc.stub.MetadataUtils +import io.grpc.stub.StreamObserver +import io.micronaut.context.ApplicationContext +import io.micronaut.context.annotation.Factory +import io.micronaut.context.annotation.Requires +import io.micronaut.context.env.Environment +import io.micronaut.grpc.annotation.GrpcChannel +import io.micronaut.grpc.server.GrpcEmbeddedServer +import io.micronaut.grpc.server.GrpcServerChannel import io.micronaut.security.authentication.UserDetails import io.micronaut.security.token.jwt.generator.JwtTokenGenerator -import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification +import spock.lang.Unroll import javax.inject.Inject +import javax.inject.Singleton -@MicronautTest -@Property(name = "grpc.server.security.jwt.enabled", value = "true") -@Property(name = "micronaut.security.token.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "true") -@Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") class GrpcServerSecurityJwtInterceptorSpec extends Specification { - @Inject - private JwtTokenGenerator jwtTokenGenerator + private static final REQUIRED_ENV = "greeter-hello-world-jwt" + private static final Map defaultConfigurations = [ + "grpc.server.security.token.jwt.enabled": true, + "micronaut.security.enabled": true, + "micronaut.security.token.enabled": true, + "micronaut.security.token.jwt.enabled": true, + "micronaut.security.token.jwt.signatures.secret.generator.secret": "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t", + "micronaut.security.token.jwt.signatures.secret.generator.base64": false, + "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm": "HS512" + ] + + def "test order configuration respected"() { + given: + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.interceptor-order", 100) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + + when: + def interceptor = context.getBean(GrpcServerSecurityJwtInterceptor) + + then: + interceptor.order == 100 + + cleanup: + embeddedServer.close() + context.stop() + } + + @Unroll + def "test valid JWT works when required role = isAuthenticated() - pattern matches"(String pattern) { + given: + List interceptMethodPatterns = [ + [pattern: pattern, access: ["isAuthenticated()"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + + when: + def testBean = context.getBean(TestBean) + + then: + testBean.sayHelloWithJwt("Brian") == "Hello Brian" - @Inject - private GrpcServerSecurityJwtInterceptor interceptor + cleanup: + embeddedServer.close() + context.stop() - def "test interceptor configured correctly"() { - expect: - interceptor.order == Ordered.HIGHEST_PRECEDENCE - interceptor.metadataKey == Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER) + where: + pattern << ["helloworld.Greeter/SayHello", "helloworld.Greeter/.*", "helloworld.*"] } - def "test interceptCall - missing JWT metadata key"() { + def "test valid JWT works when required role = isAuthenticated() and custom metadata key name"() { given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - ServerCallHandler mockServerCallHandler = Mock() + List interceptMethodPatterns = [ + [pattern: ".*", access: ["isAuthenticated()"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.metadata-key-name", "AUTH") + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() when: - interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + def testBean = context.getBean(TestBean) then: - 0 * _ + testBean.sayHelloWithCustomJwt("AUTH", "Brian") == "Hello Brian" + + cleanup: + embeddedServer.close() + context.stop() + } + + def "test valid JWT works when ROLE_HELLO required"() { + given: + List interceptMethodPatterns = [ + [pattern: ".*", access: ["ROLE_HELLO"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + + when: + def testBean = context.getBean(TestBean) + + then: + testBean.sayHelloWithJwt("Brian", ["ROLE_HELLO"]) == "Hello Brian" + + cleanup: + embeddedServer.close() + context.stop() + } + + def "test valid JWT works with no roles configured and reject-not-found = false"() { + given: + Map config = new HashMap<>(defaultConfigurations) + config.put("micronaut.security.reject-not-found", false) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + + when: + def testBean = context.getBean(TestBean) - and: + then: + testBean.sayHelloWithJwt("Brian", ["ROLE_HELLO"]) == "Hello Brian" + + cleanup: + embeddedServer.close() + context.stop() + } + + @Unroll + def "test valid JWT works with no roles found matching pattern and reject-not-found = false"(String pattern) { + given: + List interceptMethodPatterns = [ + [pattern: pattern, access: ["ROLE_HELLO"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.reject-not-found", false) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + + when: + def testBean = context.getBean(TestBean) + + then: + testBean.sayHelloWithJwt("Brian", ["ROLE_HELLO"]) == "Hello Brian" + + cleanup: + embeddedServer.close() + context.stop() + + where: + pattern << ["helloworld.Greeter/SayGoodbye", "helloworld.Greeter/Talk.*", "helloearth.*"] + } + + def "test valid JWT works with no matching roles configured and reject-not-found = false"() { + given: + List interceptMethodPatterns = [ + [pattern: "example.Foo/get.*", access: ["ROLE_HELLO"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.reject-not-found", false) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + + when: + def testBean = context.getBean(TestBean) + + then: + testBean.sayHelloWithJwt("Brian") == "Hello Brian" + + cleanup: + embeddedServer.close() + context.stop() + } + + def "test valid JWT denied when required roles = denyAll()"() { + given: + List interceptMethodPatterns = [ + [pattern: ".*", access: ["ROLE_HELLO", "denyAll()"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + def testBean = context.getBean(TestBean) + + when: + testBean.sayHelloWithJwt("Brian", ["ROLE_HELLO"]) + + then: StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) - statusRuntimeException.status.code == Status.UNAUTHENTICATED.code - statusRuntimeException.status.description == "${GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME.toLowerCase()} key missing in gRPC metadata" + statusRuntimeException.status.code == Status.Code.PERMISSION_DENIED + + cleanup: + embeddedServer.close() + context.stop() } - def "test interceptCall - invalid JWT"() { + def "test valid JWT denied when missing required role = ROLE_HELLO"() { given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - String jwt = "invalid-token" - metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) - ServerCallHandler mockServerCallHandler = Mock() + List interceptMethodPatterns = [ + [pattern: ".*", access: ["ROLE_HELLO"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + def testBean = context.getBean(TestBean) when: - interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + testBean.sayHelloWithJwt("Brian", ["ROLE_OTHER"]) == "Hello Brian" then: - 0 * _ + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.Code.PERMISSION_DENIED + + cleanup: + embeddedServer.close() + context.stop() + } + + def "test valid JWT denied when required roles = denyAll() - custom status"() { + given: + List interceptMethodPatterns = [ + [pattern: ".*", access: ["ROLE_HELLO", "denyAll()"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.failed-validation-token-status", "ABORTED") + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + def testBean = context.getBean(TestBean) + + when: + testBean.sayHelloWithJwt("Brian", ["ROLE_HELLO"]) - and: + then: StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) - statusRuntimeException.status.code == Status.PERMISSION_DENIED.code - statusRuntimeException.status.description == "JWT validation failed" + statusRuntimeException.status.code == Status.Code.ABORTED + + cleanup: + embeddedServer.close() + context.stop() } - def "test interceptCall - invalid claims JWT"() { + def "test valid JWT denied when no roles configured"() { given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - String jwt = jwtTokenGenerator.generateToken([:]).get() - metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) - ServerCallHandler mockServerCallHandler = Mock() + Map config = new HashMap<>(defaultConfigurations) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + def testBean = context.getBean(TestBean) when: - interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + testBean.sayHelloWithJwt("Brian", ["ROLE_HELLO"]) then: - 0 * _ + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.Code.PERMISSION_DENIED + + cleanup: + embeddedServer.close() + context.stop() + } - and: + def "test valid JWT denied when no subject / claims present"() { + given: + List interceptMethodPatterns = [ + [pattern: ".*", access: ["ROLE_HELLO", "isAuthenticated()"]] + ] + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + def testBean = context.getBean(TestBean) + + when: + testBean.sayHelloWithJwtNoSubject("Brian") + + then: StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) - statusRuntimeException.status.code == Status.PERMISSION_DENIED.code - statusRuntimeException.status.description == "JWT validation failed" + statusRuntimeException.status.code == Status.Code.PERMISSION_DENIED + + cleanup: + embeddedServer.close() + context.stop() } - def "test interceptCall - expired JWT"() { + def "test missing JWT works when role = isAnonymous()"() { given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - UserDetails userDetails = new UserDetails("micronaut", []) - int expiration = 1 - String jwt = jwtTokenGenerator.generateToken(userDetails, 1).get() - metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) - ServerCallHandler mockServerCallHandler = Mock() + List interceptMethodPatterns = [ + [pattern: ".*", access: ["isAnonymous()"]] + ]; + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() when: - sleep((expiration * 1000) + 500) // Allow for token to expire - interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + def testBean = context.getBean(TestBean) then: - 0 * _ + testBean.sayHelloWithoutJwt("Brian") == "Hello Brian" + + cleanup: + embeddedServer.close() + context.stop() + } + + @Unroll + def "test missing JWT denied when role = #role"(String role) { + given: + List interceptMethodPatterns = [ + [pattern: ".*", access: [role]] + ]; + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + def testBean = context.getBean(TestBean) - and: + when: + testBean.sayHelloWithoutJwt("Brian") + + then: StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) - statusRuntimeException.status.code == Status.PERMISSION_DENIED.code - statusRuntimeException.status.description == "JWT validation failed" + statusRuntimeException.status.code == Status.UNAUTHENTICATED.code + + cleanup: + embeddedServer.close() + context.stop() + + where: + role << ["isAuthenticated()", "ROLE_HELLO"] } - def "test interceptCall - valid JWT"() { + @Unroll + def "test missing JWT denied when role = #role - custom status"(String role) { given: - ServerCall mockServerCall = Mock() - Metadata metadata = new Metadata() - UserDetails userDetails = new UserDetails("micronaut", ["admin"]) - String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() - metadata.put(Metadata.Key.of(GrpcServerSecurityJwtConfiguration.DEFAULT_METADATA_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER), jwt) - ServerCallHandler mockServerCallHandler = Mock() + List interceptMethodPatterns = [ + [pattern: ".*", access: [role]] + ]; + Map config = new HashMap<>(defaultConfigurations) + config.put("grpc.server.security.token.jwt.missing-token-status", "NOT_FOUND") + config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + def testBean = context.getBean(TestBean) when: - ServerCall.Listener serverCallListener = interceptor.interceptCall(mockServerCall, metadata, mockServerCallHandler) + testBean.sayHelloWithoutJwt("Brian") then: - 1 * mockServerCallHandler.startCall(mockServerCall, metadata) >> Mock( ServerCall.Listener) - 0 * _ + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.NOT_FOUND.code + + cleanup: + embeddedServer.close() + context.stop() + + where: + role << ["isAuthenticated()", "ROLE_HELLO"] + } + + def "test invalid JWT denied"() { + given: + Map config = new HashMap<>(defaultConfigurations) + def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) + def embeddedServer = context.getBean(GrpcEmbeddedServer) + embeddedServer.start() + def testBean = context.getBean(TestBean) + + when: + testBean.sayHelloWithNonParseableJwt("Brian") + + then: + StatusRuntimeException statusRuntimeException = thrown(StatusRuntimeException) + statusRuntimeException.status.code == Status.Code.PERMISSION_DENIED + + cleanup: + embeddedServer.close() + context.stop() + } + + @Factory + @Requires(env = REQUIRED_ENV) + static class Clients { + + @Singleton + GreeterGrpc.GreeterBlockingStub blockingStub(@GrpcChannel(GrpcServerChannel.NAME) Channel channel) { + GreeterGrpc.newBlockingStub(channel) + } + + } + + @Singleton + @Requires(env = REQUIRED_ENV) + static class TestBean { + + @Inject + JwtTokenGenerator jwtTokenGenerator + + @Inject + GreeterGrpc.GreeterBlockingStub blockingStub + + String sayHelloWithJwt(String message, final List roles = []) { + UserDetails userDetails = new UserDetails("micronaut", roles) + String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() + Metadata metadata = new Metadata() + metadata.put(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER), jwt) + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + MetadataUtils.attachHeaders(blockingStub, metadata).sayHello(helloRequest).message + } + + String sayHelloWithCustomJwt(String metadataKeyName, String message, final List roles = []) { + UserDetails userDetails = new UserDetails("micronaut", roles) + String jwt = jwtTokenGenerator.generateToken(userDetails, 60).get() + Metadata metadata = new Metadata() + metadata.put(Metadata.Key.of(metadataKeyName, Metadata.ASCII_STRING_MARSHALLER), jwt) + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + MetadataUtils.attachHeaders(blockingStub, metadata).sayHello(helloRequest).message + } + + String sayHelloWithJwtNoSubject(String message) { + String jwt = jwtTokenGenerator.generateToken([:]).get() + Metadata metadata = new Metadata() + metadata.put(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER), jwt) + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + MetadataUtils.attachHeaders(blockingStub, metadata).sayHello(helloRequest).message + } + + String sayHelloWithNonParseableJwt(String message) { + Metadata metadata = new Metadata() + metadata.put(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER), "invalid-jwt-which-cannot-be-parsed") + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + MetadataUtils.attachHeaders(blockingStub, metadata).sayHello(helloRequest).message + } + + String sayHelloWithoutJwt(String message) { + HelloRequest helloRequest = HelloRequest.newBuilder().setName(message).build() + blockingStub.sayHello(helloRequest).message + } + + } + + @Singleton + @Requires(env = REQUIRED_ENV) + static class GreeterImpl extends GreeterGrpc.GreeterImplBase { + + @Override + void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + request.getName()).build(); + responseObserver.onNext(reply) + responseObserver.onCompleted() + } - and: - serverCallListener - serverCallListener instanceof ForwardingServerCallListener.SimpleForwardingServerCallListener } -} +} \ No newline at end of file diff --git a/src/main/docs/guide/serverSecurity.adoc b/src/main/docs/guide/serverSecurity.adoc index d22ca421a..42e53a316 100644 --- a/src/main/docs/guide/serverSecurity.adoc +++ b/src/main/docs/guide/serverSecurity.adoc @@ -4,17 +4,17 @@ Currently, only `JWT` is supported for server-side security. ==== gRPC Server Security JWT JWT security is accomplished by an implementation of `io.grpc.ServerInterceptor`. -If you are already using `io.micronaut.security:micronaut-security-jwt`, then configuration will be quite simple: +If you are already using `io.micronaut.security:micronaut-security-jwt`, then no additional configuration will be required for a basic implemntation. ===== Configuration -.Enable JWT Validation +.Disabling JWT Validation [source,yaml] ---- -grpc.server.security.jwt: - enabled: true # <1> +grpc.server.security.token.jwt: + enabled: false # <1> ---- -<1> By default, this is disabled +<1> By default, this is enabled If you are not already using `io.micronaut.security:micronaut-security-jwt`, then you will also need to prepare that configuration as well. Please read these https://micronaut-projects.github.io/micronaut-security/latest/guide/#jwt[docs] or this @@ -29,7 +29,7 @@ This can be changed by overriding the interceptor configuration as demonstrated .Customizing JWT interceptor [source,yaml] ---- -grpc.server.security.jwt: +grpc.server.security.token.jwt: metadata-key-name: AUTHORIZATION # <1> interceptor-order: 100 # <2> ---- @@ -44,10 +44,14 @@ This can be changed by overriding the patterns configuration as demonstrated bel .Customizing Method Pattern Filters [source,yaml] ---- -grpc.server.security.jwt: +grpc.server.security.token.jwt: intercept-method-patterns: - - app.ServiceName/MethodName # <1> - - app.OtherServiceName/.* # <2> + - pattern: app.ServiceName/MethodName # <1> + access: + - isAuthenticated() + - pattern: app.OtherServiceName/.* # <2> + access: + - ROLE_OTHER ---- <1> Matches an exact fully qualified gRPC Service Method Name - see https://grpc.github.io/grpc-java/javadoc/io/grpc/MethodDescriptor.html#getFullMethodName--[gRPC Javadocs]. <2> Matches on a regular expression of the fully qualified gRPC service method name @@ -63,7 +67,7 @@ This can be changed by overriding the status configuration as demonstrated below .Customizing gRPC Statuses [source,yaml] ---- -grpc.server.security.jwt: +grpc.server.security.token.jwt: missing-token-status: NOT_FOUND # <1> failed-validation-token-status: UNAUTHENTICATED # <2> ---- From a0656920e9422a112db8e2a5298715f48f128f6c Mon Sep 17 00:00:00 2001 From: brianwyka Date: Wed, 24 Feb 2021 11:33:40 -0500 Subject: [PATCH 08/12] Spotless fixes --- .../security/jwt/GrpcServerSecurityJwtInterceptorFactory.java | 1 + .../jwt/interceptor/GrpcServerSecurityJwtInterceptor.java | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java index 5b9b7d358..a9639a1e3 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java @@ -47,6 +47,7 @@ public class GrpcServerSecurityJwtInterceptorFactory { * @param signatureConfigurations the signature configurations * @param encryptionConfigurations the encryption configurations * @param genericJwtClaimsValidators the generic JWT claims validators + * @param securityConfiguration the security configuration * @param rolesFinder the roles finder for comparing roles with required roles * @return the server interceptor bean */ diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java index 77c48b528..12657331a 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java @@ -58,7 +58,6 @@ public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Orde private final boolean rejectRolesNotFound; private final RolesFinder rolesFinder; - /** * Create the interceptor based on the configuration. * @@ -165,8 +164,6 @@ private static StatusRuntimeException statusRuntimeException(final Status.Code s /** * Get the required access for the server call. * - * TODO: Is there any way we can get access to values from @Secured annotation here ?? - * * @param config the configuration * @param serverCall the server call * @param the request type From 695fcdccf7698e4caaa8e220e81e769cf00fbe1a Mon Sep 17 00:00:00 2001 From: brianwyka Date: Tue, 2 Mar 2021 18:16:14 -0500 Subject: [PATCH 09/12] Update @Requires to reference corresponding configurations --- .../GrpcServerSecurityJwtConfiguration.java | 8 +- ...ecurityJwtConfigurationDisabledSpec.groovy | 99 ------------------- ...SecurityJwtConfigurationEnabledSpec.groovy | 2 - ...ecurityJwtConfigurationOverrideSpec.groovy | 2 - ...erSecurityJwtInterceptorFactorySpec.groovy | 4 - ...rpcServerSecurityJwtInterceptorSpec.groovy | 4 +- 6 files changed, 3 insertions(+), 116 deletions(-) diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index b6635860b..df38b6006 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -25,9 +25,6 @@ import io.micronaut.core.util.Toggleable; import io.micronaut.grpc.server.GrpcServerConfiguration; import io.micronaut.security.config.InterceptUrlMapPattern; -import io.micronaut.security.config.SecurityConfigurationProperties; -import io.micronaut.security.token.config.TokenConfigurationProperties; -import io.micronaut.security.token.jwt.config.JwtConfigurationProperties; import javax.validation.constraints.NotBlank; import java.util.Collection; @@ -39,9 +36,8 @@ * @author Brian Wyka */ @ConfigurationProperties(GrpcServerSecurityJwtConfiguration.PREFIX) -@Requires(property = SecurityConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) -@Requires(property = TokenConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) -@Requires(property = JwtConfigurationProperties.PREFIX + ".enabled", notEquals = StringUtils.FALSE) +@Requires(configuration = "io.micronaut.security") +@Requires(configuration = "io.micronaut.security.token.jwt") @Requires(property = GrpcServerSecurityJwtConfiguration.PREFIX + ".enabled", notEquals = StringUtils.FALSE) public interface GrpcServerSecurityJwtConfiguration extends Toggleable { diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy index d82e41951..e1a9667ed 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationDisabledSpec.groovy @@ -7,108 +7,9 @@ import spock.lang.Specification class GrpcServerSecurityJwtConfigurationDisabledSpec extends Specification { - def "beans are not loaded when security not enabled"() { - given: - def config = [ - "micronaut.security.enabled": false, - "micronaut.security.token.enabled": true, - "micronaut.security.token.jwt.enabled": true, - "grpc.server.security.token.jwt.enabled": true - ] - def context = ApplicationContext.run(config) - - when: - context.getBean(GrpcServerSecurityJwtConfiguration) - - then: - thrown(NoSuchBeanException) - - when: - context.getBean(GrpcServerSecurityJwtInterceptorFactory) - - then: - thrown(NoSuchBeanException) - - when: - context.getBean(GrpcServerSecurityJwtInterceptor) - - then: - thrown(NoSuchBeanException) - - cleanup: - context.close() - } - - def "beans are not loaded when security token not enabled"() { - given: - def config = [ - "micronaut.security.enabled": true, - "micronaut.security.token.enabled": false, - "micronaut.security.token.jwt.enabled": true, - "grpc.server.security.token.jwt.enabled": true - ] - def context = ApplicationContext.run(config) - - when: - context.getBean(GrpcServerSecurityJwtConfiguration) - - then: - thrown(NoSuchBeanException) - - when: - context.getBean(GrpcServerSecurityJwtInterceptorFactory) - - then: - thrown(NoSuchBeanException) - - when: - context.getBean(GrpcServerSecurityJwtInterceptor) - - then: - thrown(NoSuchBeanException) - - cleanup: - context.close() - } - - def "beans are not loaded when security token jwt not enabled"() { - given: - def config = [ - "micronaut.security.enabled": true, - "micronaut.security.token.enabled": true, - "micronaut.security.token.jwt.enabled": false, - "grpc.server.security.token.jwt.enabled": true - ] - def context = ApplicationContext.run(config) - - when: - context.getBean(GrpcServerSecurityJwtConfiguration) - - then: - thrown(NoSuchBeanException) - - when: - context.getBean(GrpcServerSecurityJwtInterceptorFactory) - - then: - thrown(NoSuchBeanException) - - when: - context.getBean(GrpcServerSecurityJwtInterceptor) - - then: - thrown(NoSuchBeanException) - - cleanup: - context.close() - } - def "beans are not loaded when grpc security jwt not enabled"() { given: def config = [ - "micronaut.security.enabled": true, - "micronaut.security.token.enabled": true, - "micronaut.security.token.jwt.enabled": true, "grpc.server.security.token.jwt.enabled": false ] def context = ApplicationContext.run(config) diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy index fe02c1316..0cfc83779 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy @@ -12,8 +12,6 @@ import spock.lang.Specification import javax.inject.Inject @MicronautTest -@Property(name = "micronaut.security.enabled", value = "true") -@Property(name = "micronaut.security.token.enabled", value = "true") @Property(name = "grpc.server.security.token.jwt.enabled", value = "true") class GrpcServerSecurityJwtConfigurationEnabledSpec extends Specification { diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy index 240f7379d..048656761 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationOverrideSpec.groovy @@ -10,8 +10,6 @@ import spock.lang.Specification import javax.inject.Inject @MicronautTest -@Property(name = "micronaut.security.enabled", value = "true") -@Property(name = "micronaut.security.token.enabled", value = "true") @Property(name = "grpc.server.security.token.jwt.enabled", value = "true") @Property(name = "grpc.server.security.token.jwt.metadata-key-name", value = "AUTH") @Property(name = "grpc.server.security.token.jwt.missing-token-status", value = "NOT_FOUND") diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy index 100801d94..ea0274964 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactorySpec.groovy @@ -9,11 +9,7 @@ import spock.lang.Specification import javax.inject.Inject @MicronautTest -@Property(name = "micronaut.security.enabled", value = "true") -@Property(name = "micronaut.security.token.enabled", value = "true") @Property(name = "grpc.server.security.token.jwt.enabled", value = "true") -@Property(name = "micronaut.security.token.enabled", value = "true") -@Property(name = "micronaut.security.token.jwt.enabled", value = "true") @Property(name = "micronaut.security.token.jwt.signatures.secret.generator.secret", value = "SeCr3t") @Property(name = "micronaut.security.token.jwt.signatures.secret.generator.base64", value = "false") @Property(name = "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm", value = "HS512") diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy index 873a56740..0ff8de86b 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy @@ -28,10 +28,8 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { private static final REQUIRED_ENV = "greeter-hello-world-jwt" private static final Map defaultConfigurations = [ + "micronaut.security.token.roles-name": true, "grpc.server.security.token.jwt.enabled": true, - "micronaut.security.enabled": true, - "micronaut.security.token.enabled": true, - "micronaut.security.token.jwt.enabled": true, "micronaut.security.token.jwt.signatures.secret.generator.secret": "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t", "micronaut.security.token.jwt.signatures.secret.generator.base64": false, "micronaut.security.token.jwt.signatures.secret.generator.jws-algorithm": "HS512" From 1143ca871680b57604b2e160c0f9f6d10b2cc523 Mon Sep 17 00:00:00 2001 From: brianwyka Date: Wed, 3 Mar 2021 08:31:53 -0800 Subject: [PATCH 10/12] Reuse intercept-url-map from SecurityConfiguration --- .../GrpcServerSecurityJwtConfiguration.java | 11 --------- ...pcServerSecurityJwtInterceptorFactory.java | 3 +-- .../GrpcServerSecurityJwtInterceptor.java | 21 ++++++++-------- ...SecurityJwtConfigurationEnabledSpec.groovy | 1 - ...rpcServerSecurityJwtInterceptorSpec.groovy | 24 +++++++++---------- src/main/docs/guide/serverSecurity.adoc | 9 ++++--- 6 files changed, 30 insertions(+), 39 deletions(-) diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index df38b6006..56d15ccdf 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -60,17 +60,6 @@ public interface GrpcServerSecurityJwtConfiguration extends Toggleable { @Bindable(defaultValue = "" + Ordered.HIGHEST_PRECEDENCE) int getInterceptorOrder(); - /** - * Get the list of fully qualified RPC method patterns which should be intercepted and interrogated for a valid JWT. - * If no values are provided, by default, all methods will be intercepted. - * - * @see io.grpc.MethodDescriptor#getFullMethodName() - * - * @return the intercept method names. - */ - @Nullable - Collection getInterceptMethodPatterns(); - /** * The {@link Status} returned by the interceptor when JWT is missing from metadata. * The default value is {@link Status.Code#UNAUTHENTICATED} diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java index a9639a1e3..3e0b2a01a 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java @@ -64,8 +64,7 @@ public ServerInterceptor serverInterceptor(final GrpcServerSecurityJwtConfigurat .withEncryptions(encryptionConfigurations) .withClaimValidators(genericJwtClaimsValidators) .build(); - final boolean rejectRolesNotFound = securityConfiguration.isRejectNotFound(); - return new GrpcServerSecurityJwtInterceptor(grpcServerSecurityJwtConfiguration, jwtValidator, rolesFinder, rejectRolesNotFound); + return new GrpcServerSecurityJwtInterceptor(grpcServerSecurityJwtConfiguration, jwtValidator, rolesFinder, securityConfiguration); } } diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java index 12657331a..90be6c250 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java @@ -27,6 +27,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.grpc.server.security.jwt.GrpcServerSecurityJwtConfiguration; import io.micronaut.security.config.InterceptUrlMapPattern; +import io.micronaut.security.config.SecurityConfiguration; import io.micronaut.security.rules.SecurityRule; import io.micronaut.security.token.RolesFinder; import io.micronaut.security.token.jwt.generator.claims.JwtClaimsSetAdapter; @@ -55,6 +56,7 @@ public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Orde private final GrpcServerSecurityJwtConfiguration config; private final Metadata.Key jwtMetadataKey; private final JwtValidator jwtValidator; + private final List interceptMethodPatterns; // httpMethod is not used in this context private final boolean rejectRolesNotFound; private final RolesFinder rolesFinder; @@ -64,16 +66,17 @@ public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Orde * @param config the gRPC Security JWT configuration * @param jwtValidator the JWT validator * @param rolesFinder the roles finder - * @param rejectRolesNotFound whether or not to reject request if no roles found + * @param securityConfiguration the security configuration */ public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration config, final JwtValidator jwtValidator, final RolesFinder rolesFinder, - final boolean rejectRolesNotFound) { + final SecurityConfiguration securityConfiguration) { this.config = config; this.jwtMetadataKey = Metadata.Key.of(config.getMetadataKeyName(), Metadata.ASCII_STRING_MARSHALLER); this.jwtValidator = jwtValidator; - this.rejectRolesNotFound = rejectRolesNotFound; + this.interceptMethodPatterns = securityConfiguration.getInterceptUrlMap(); + this.rejectRolesNotFound = securityConfiguration.isRejectNotFound(); this.rolesFinder = rolesFinder; } @@ -90,8 +93,8 @@ public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration */ @Override public ServerCall.Listener interceptCall(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { - final List requiredAccesses = getRequiredAccesses(config, call); - if (CollectionUtils.isEmpty(config.getInterceptMethodPatterns()) && !rejectRolesNotFound) { + final List requiredAccesses = getRequiredAccesses(call); + if (CollectionUtils.isEmpty(interceptMethodPatterns) && !rejectRolesNotFound) { LOG.debug("JWT validation is skipped due to 'intercept-method-patterns' configuration being empty"); return forward(call, metadata, next); } else if (requiredAccesses.isEmpty() && rejectRolesNotFound) { @@ -164,18 +167,16 @@ private static StatusRuntimeException statusRuntimeException(final Status.Code s /** * Get the required access for the server call. * - * @param config the configuration * @param serverCall the server call * @param the request type * @param the response type * @return the required access */ - private static List getRequiredAccesses(final GrpcServerSecurityJwtConfiguration config, final ServerCall serverCall) { - if (CollectionUtils.isEmpty(config.getInterceptMethodPatterns())) { + private List getRequiredAccesses(final ServerCall serverCall) { + if (CollectionUtils.isEmpty(interceptMethodPatterns)) { return Collections.emptyList(); } - return config.getInterceptMethodPatterns() - .stream() + return interceptMethodPatterns.stream() .filter(interceptMethodPattern -> serverCall.getMethodDescriptor().getFullMethodName().matches(interceptMethodPattern.getPattern())) .map(InterceptUrlMapPattern::getAccess) .flatMap(Collection::stream) diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy index 0cfc83779..20871a959 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfigurationEnabledSpec.groovy @@ -32,7 +32,6 @@ class GrpcServerSecurityJwtConfigurationEnabledSpec extends Specification { config.metadataKeyName == "JWT" config.missingTokenStatus == Status.UNAUTHENTICATED.code config.failedValidationTokenStatus == Status.PERMISSION_DENIED.code - !config.interceptMethodPatterns config.interceptorOrder == Ordered.HIGHEST_PRECEDENCE } diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy index 0ff8de86b..b7a6a816a 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy @@ -61,7 +61,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: pattern, access: ["isAuthenticated()"]] ] Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -87,7 +87,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { ] Map config = new HashMap<>(defaultConfigurations) config.put("grpc.server.security.token.jwt.metadata-key-name", "AUTH") - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -109,7 +109,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: ".*", access: ["ROLE_HELLO"]] ] Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -151,7 +151,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: pattern, access: ["ROLE_HELLO"]] ] Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) config.put("micronaut.security.reject-not-found", false) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) @@ -177,7 +177,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: "example.Foo/get.*", access: ["ROLE_HELLO"]] ] Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) config.put("micronaut.security.reject-not-found", false) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) @@ -200,7 +200,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: ".*", access: ["ROLE_HELLO", "denyAll()"]] ] Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -224,7 +224,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: ".*", access: ["ROLE_HELLO"]] ] Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -249,7 +249,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { ] Map config = new HashMap<>(defaultConfigurations) config.put("grpc.server.security.token.jwt.failed-validation-token-status", "ABORTED") - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -293,7 +293,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: ".*", access: ["ROLE_HELLO", "isAuthenticated()"]] ] Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -317,7 +317,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: ".*", access: ["isAnonymous()"]] ]; Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -340,7 +340,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { [pattern: ".*", access: [role]] ]; Map config = new HashMap<>(defaultConfigurations) - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() @@ -369,7 +369,7 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { ]; Map config = new HashMap<>(defaultConfigurations) config.put("grpc.server.security.token.jwt.missing-token-status", "NOT_FOUND") - config.put("grpc.server.security.token.jwt.intercept-method-patterns", interceptMethodPatterns) + config.put("micronaut.security.intercept-url-map", interceptMethodPatterns) def context = ApplicationContext.run(config, REQUIRED_ENV, Environment.TEST) def embeddedServer = context.getBean(GrpcEmbeddedServer) embeddedServer.start() diff --git a/src/main/docs/guide/serverSecurity.adoc b/src/main/docs/guide/serverSecurity.adoc index 42e53a316..ef2806aca 100644 --- a/src/main/docs/guide/serverSecurity.adoc +++ b/src/main/docs/guide/serverSecurity.adoc @@ -39,13 +39,13 @@ grpc.server.security.token.jwt: ===== Interceptor Patterns By default, all gRPC services and methods will be intercepted. -This can be changed by overriding the patterns configuration as demonstrated below: +This can be changed by overriding the `intercept-url-map` configuration as demonstrated below: .Customizing Method Pattern Filters [source,yaml] ---- -grpc.server.security.token.jwt: - intercept-method-patterns: +micronaut.security: + intercept-url-map: - pattern: app.ServiceName/MethodName # <1> access: - isAuthenticated() @@ -56,6 +56,9 @@ grpc.server.security.token.jwt: <1> Matches an exact fully qualified gRPC Service Method Name - see https://grpc.github.io/grpc-java/javadoc/io/grpc/MethodDescriptor.html#getFullMethodName--[gRPC Javadocs]. <2> Matches on a regular expression of the fully qualified gRPC service method name +NOTE: This configuration can be shared with `URL` filtering on HTTP requests as well. For more information on this configuration, +please see https://micronaut-projects.github.io/micronaut-security/latest/guide/#interceptUrlMap[Micronaut Security documentation]. + ===== Interceptor Behavior The server interceptor will throw `io.grpc.StatusRuntimeException` when there is any issue with validating the `JWT`. From 8cfb489aa8a2501392c286ecc3ca3600052241e9 Mon Sep 17 00:00:00 2001 From: brianwyka Date: Wed, 3 Mar 2021 08:33:42 -0800 Subject: [PATCH 11/12] Cleanup imports --- .../security/jwt/GrpcServerSecurityJwtConfiguration.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index 56d15ccdf..ff27490da 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -15,7 +15,6 @@ */ package io.micronaut.grpc.server.security.jwt; -import edu.umd.cs.findbugs.annotations.Nullable; import io.grpc.Status; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Requires; @@ -24,10 +23,8 @@ import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; import io.micronaut.grpc.server.GrpcServerConfiguration; -import io.micronaut.security.config.InterceptUrlMapPattern; import javax.validation.constraints.NotBlank; -import java.util.Collection; /** * gRPC Security JWT configuration. From 7c676d0092cbef6b7cb8f432d980d72ddbe4fb3b Mon Sep 17 00:00:00 2001 From: brianwyka Date: Fri, 5 Mar 2021 08:51:58 -0500 Subject: [PATCH 12/12] Add grpc-server-security module and duplicate some classes / functionality from micronaut-security --- grpc-server-security-jwt/build.gradle | 1 + .../GrpcServerSecurityJwtConfiguration.java | 34 +-- ...pcServerSecurityJwtInterceptorFactory.java | 70 ------ .../JwtGrpcServerAuthenticationFetcher.java | 60 +++++ .../GrpcServerSecurityJwtInterceptor.java | 210 ------------------ ...GrpcServerAuthenticationFetcherSpec.groovy | 9 + ...rpcServerSecurityJwtInterceptorSpec.groovy | 1 - grpc-server-security/build.gradle | 19 ++ .../AbstractGrpcServerSecurityRule.java | 95 ++++++++ .../GrpcServerAuthenticationFetcher.java | 41 ++++ .../GrpcServerSecurityConfiguration.java | 55 +++++ .../GrpcServerSecurityInterceptor.java | 186 ++++++++++++++++ .../security/GrpcServerSecurityRule.java | 46 ++++ ...InterceptUrlMapGrpcServerSecurityRule.java | 86 +++++++ .../AbstractGrpcServerSecurityRuleSpec.groovy | 9 + ...GrpcServerSecurityConfigurationSpec.groovy | 9 + .../GrpcServerSecurityInterceptorSpec.groovy | 9 + ...eptUrlMapGrpcServerSecurityRuleSpec.groovy | 9 + .../src/test/resources/logback.xml | 17 ++ settings.gradle | 4 +- 20 files changed, 657 insertions(+), 313 deletions(-) delete mode 100644 grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java create mode 100644 grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/JwtGrpcServerAuthenticationFetcher.java delete mode 100644 grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java create mode 100644 grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/JwtGrpcServerAuthenticationFetcherSpec.groovy create mode 100644 grpc-server-security/build.gradle create mode 100644 grpc-server-security/src/main/java/io/micronaut/grpc/server/security/AbstractGrpcServerSecurityRule.java create mode 100644 grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerAuthenticationFetcher.java create mode 100644 grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityConfiguration.java create mode 100644 grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityInterceptor.java create mode 100644 grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityRule.java create mode 100644 grpc-server-security/src/main/java/io/micronaut/grpc/server/security/InterceptUrlMapGrpcServerSecurityRule.java create mode 100644 grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/AbstractGrpcServerSecurityRuleSpec.groovy create mode 100644 grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/GrpcServerSecurityConfigurationSpec.groovy create mode 100644 grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/GrpcServerSecurityInterceptorSpec.groovy create mode 100644 grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/InterceptUrlMapGrpcServerSecurityRuleSpec.groovy create mode 100644 grpc-server-security/src/test/resources/logback.xml diff --git a/grpc-server-security-jwt/build.gradle b/grpc-server-security-jwt/build.gradle index fd05df0ce..3ca070937 100644 --- a/grpc-server-security-jwt/build.gradle +++ b/grpc-server-security-jwt/build.gradle @@ -10,6 +10,7 @@ dependencies { testAnnotationProcessor "io.micronaut.security:micronaut-security-annotations:$micronautSecurityVersion" api project(":grpc-server-runtime") + api project(":grpc-server-security") api "io.micronaut:micronaut-inject:$micronautVersion" api "io.micronaut:micronaut-runtime:$micronautVersion" api "com.nimbusds:nimbus-jose-jwt:9.4.2" diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java index ff27490da..3793341b3 100644 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtConfiguration.java @@ -15,14 +15,12 @@ */ package io.micronaut.grpc.server.security.jwt; -import io.grpc.Status; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Requires; import io.micronaut.core.bind.annotation.Bindable; -import io.micronaut.core.order.Ordered; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.Toggleable; -import io.micronaut.grpc.server.GrpcServerConfiguration; +import io.micronaut.grpc.server.security.GrpcServerSecurityConfiguration; import javax.validation.constraints.NotBlank; @@ -35,46 +33,20 @@ @ConfigurationProperties(GrpcServerSecurityJwtConfiguration.PREFIX) @Requires(configuration = "io.micronaut.security") @Requires(configuration = "io.micronaut.security.token.jwt") +@Requires(configuration = "io.micronaut.grpc.server.security") @Requires(property = GrpcServerSecurityJwtConfiguration.PREFIX + ".enabled", notEquals = StringUtils.FALSE) public interface GrpcServerSecurityJwtConfiguration extends Toggleable { /** * The configuration prefix. */ - String PREFIX = GrpcServerConfiguration.PREFIX + ".security.token.jwt"; + String PREFIX = GrpcServerSecurityConfiguration.PREFIX + ".token.jwt"; /** * The default name for the JWT metadata key. */ String DEFAULT_METADATA_KEY_NAME = "JWT"; - /** - * The order to be applied to the server interceptor in the interceptor chain. Defaults - * to {@value io.micronaut.core.order.Ordered#HIGHEST_PRECEDENCE} if not configured. - * - * @return the order - */ - @Bindable(defaultValue = "" + Ordered.HIGHEST_PRECEDENCE) - int getInterceptorOrder(); - - /** - * The {@link Status} returned by the interceptor when JWT is missing from metadata. - * The default value is {@link Status.Code#UNAUTHENTICATED} - * - * @return the status - */ - @Bindable(defaultValue = "UNAUTHENTICATED") - Status.Code getMissingTokenStatus(); - - /** - * The {@link Status} returned by the interceptor when JWT validation fails. The - * default value is {@link Status.Code#PERMISSION_DENIED} - * - * @return the status - */ - @Bindable(defaultValue = "PERMISSION_DENIED") - Status.Code getFailedValidationTokenStatus(); - /** * The name of the metadata key which holds the JWT. Defaults * to {@value #DEFAULT_METADATA_KEY_NAME} if not configured. diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java deleted file mode 100644 index 3e0b2a01a..000000000 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/GrpcServerSecurityJwtInterceptorFactory.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2017-2021 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.grpc.server.security.jwt; - -import io.grpc.ServerInterceptor; -import io.micronaut.context.annotation.Bean; -import io.micronaut.context.annotation.Factory; -import io.micronaut.context.annotation.Requires; -import io.micronaut.grpc.server.security.jwt.interceptor.GrpcServerSecurityJwtInterceptor; -import io.micronaut.security.config.SecurityConfiguration; -import io.micronaut.security.token.RolesFinder; -import io.micronaut.security.token.jwt.encryption.EncryptionConfiguration; -import io.micronaut.security.token.jwt.signature.SignatureConfiguration; -import io.micronaut.security.token.jwt.validator.GenericJwtClaimsValidator; -import io.micronaut.security.token.jwt.validator.JwtValidator; - -import javax.inject.Singleton; -import java.util.Collection; - -/** - * Factory for creating instances of gRPC server security JWT interceptors. - * - * @since 2.4.0 - * @author Brian Wyka - */ -@Factory -@Requires(beans = GrpcServerSecurityJwtConfiguration.class) -public class GrpcServerSecurityJwtInterceptorFactory { - - /** - * Constructs an instance of {@link GrpcServerSecurityJwtInterceptor} based on configuration. - * - * @param grpcServerSecurityJwtConfiguration the gRPC server security JWT configuration - * @param signatureConfigurations the signature configurations - * @param encryptionConfigurations the encryption configurations - * @param genericJwtClaimsValidators the generic JWT claims validators - * @param securityConfiguration the security configuration - * @param rolesFinder the roles finder for comparing roles with required roles - * @return the server interceptor bean - */ - @Bean - @Singleton - public ServerInterceptor serverInterceptor(final GrpcServerSecurityJwtConfiguration grpcServerSecurityJwtConfiguration, - final Collection signatureConfigurations, - final Collection encryptionConfigurations, - final Collection genericJwtClaimsValidators, - final SecurityConfiguration securityConfiguration, - final RolesFinder rolesFinder) { - final JwtValidator jwtValidator = JwtValidator.builder() - .withSignatures(signatureConfigurations) - .withEncryptions(encryptionConfigurations) - .withClaimValidators(genericJwtClaimsValidators) - .build(); - return new GrpcServerSecurityJwtInterceptor(grpcServerSecurityJwtConfiguration, jwtValidator, rolesFinder, securityConfiguration); - } - -} diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/JwtGrpcServerAuthenticationFetcher.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/JwtGrpcServerAuthenticationFetcher.java new file mode 100644 index 000000000..87bccbf94 --- /dev/null +++ b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/JwtGrpcServerAuthenticationFetcher.java @@ -0,0 +1,60 @@ +package io.micronaut.grpc.server.security.jwt; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.micronaut.grpc.server.security.GrpcServerAuthenticationFetcher; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.token.jwt.validator.JwtAuthenticationFactory; +import io.micronaut.security.token.jwt.validator.JwtValidator; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Optional; + +/** + * Fetches {@link Authentication} from JWT present in the gRPC {@link Metadata}. + * + * @since 2.4.0 + * @author Brian Wyka + */ +@Singleton +public class JwtGrpcServerAuthenticationFetcher implements GrpcServerAuthenticationFetcher { + + private final Metadata.Key jwtMetadataKey; + private final JwtValidator jwtValidator; + private final JwtAuthenticationFactory jwtAuthenticationFactory; + + /** + * Constructs the authentication fetcher with the provided configuration and JWT validator. + * + * @param config the configuration + * @param jwtValidator the JWT validator + * @param jwtAuthenticationFactory the JWT authentication factory + */ + @Inject + public JwtGrpcServerAuthenticationFetcher(final GrpcServerSecurityJwtConfiguration config, + final JwtValidator jwtValidator, + final JwtAuthenticationFactory jwtAuthenticationFactory) { + this.jwtMetadataKey = Metadata.Key.of(config.getMetadataKeyName(), Metadata.ASCII_STRING_MARSHALLER); + this.jwtValidator = jwtValidator; + this.jwtAuthenticationFactory = jwtAuthenticationFactory; + } + + /** + * Fetch the {@link Authentication} from JWT in metadata. + * + * @param serverCall {@link ServerCall} being executed. + * @param metadata the metadata to retrieve JWT from + * @param the type of the server call request + * @param the type of the server call response + * @return the authentication if found, otherwise {@link Optional#empty()} + */ + @Override + public Optional fetchAuthentication(final ServerCall serverCall, final Metadata metadata) { + return Optional.of(jwtMetadataKey) + .map(metadata::get) + .flatMap(jwt -> jwtValidator.validate(jwt, null)) + .flatMap(jwtAuthenticationFactory::createAuthentication); + } + +} diff --git a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java b/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java deleted file mode 100644 index 90be6c250..000000000 --- a/grpc-server-security-jwt/src/main/java/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptor.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2017-2021 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.grpc.server.security.jwt.interceptor; - -import com.nimbusds.jwt.JWT; -import io.grpc.ForwardingServerCallListener; -import io.grpc.Metadata; -import io.grpc.ServerCall; -import io.grpc.ServerCallHandler; -import io.grpc.ServerInterceptor; -import io.grpc.Status; -import io.grpc.StatusRuntimeException; -import io.micronaut.core.order.Ordered; -import io.micronaut.core.util.CollectionUtils; -import io.micronaut.grpc.server.security.jwt.GrpcServerSecurityJwtConfiguration; -import io.micronaut.security.config.InterceptUrlMapPattern; -import io.micronaut.security.config.SecurityConfiguration; -import io.micronaut.security.rules.SecurityRule; -import io.micronaut.security.token.RolesFinder; -import io.micronaut.security.token.jwt.generator.claims.JwtClaimsSetAdapter; -import io.micronaut.security.token.jwt.validator.JwtValidator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.ParseException; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - - -/** - * gRPC Server Security JWT Interceptor. - * - * @since 2.4.0 - * @author Brian Wyka - */ -public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Ordered { - - private static final Logger LOG = LoggerFactory.getLogger(GrpcServerSecurityJwtInterceptor.class); - - private final GrpcServerSecurityJwtConfiguration config; - private final Metadata.Key jwtMetadataKey; - private final JwtValidator jwtValidator; - private final List interceptMethodPatterns; // httpMethod is not used in this context - private final boolean rejectRolesNotFound; - private final RolesFinder rolesFinder; - - /** - * Create the interceptor based on the configuration. - * - * @param config the gRPC Security JWT configuration - * @param jwtValidator the JWT validator - * @param rolesFinder the roles finder - * @param securityConfiguration the security configuration - */ - public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration config, - final JwtValidator jwtValidator, - final RolesFinder rolesFinder, - final SecurityConfiguration securityConfiguration) { - this.config = config; - this.jwtMetadataKey = Metadata.Key.of(config.getMetadataKeyName(), Metadata.ASCII_STRING_MARSHALLER); - this.jwtValidator = jwtValidator; - this.interceptMethodPatterns = securityConfiguration.getInterceptUrlMap(); - this.rejectRolesNotFound = securityConfiguration.isRejectNotFound(); - this.rolesFinder = rolesFinder; - } - - /** - * Intercept the call to validate the JSON web token. If the token is not present in the metadata, or - * if the token is not valid, this method will deny the request with a {@link io.grpc.StatusRuntimeException}. - * - * @param call the server call - * @param metadata the metadata - * @param next the next processor in the interceptor chain - * @param the type of the server request - * @param the type of the server response - * @throws io.grpc.StatusRuntimeException if token not present or invalid - */ - @Override - public ServerCall.Listener interceptCall(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { - final List requiredAccesses = getRequiredAccesses(call); - if (CollectionUtils.isEmpty(interceptMethodPatterns) && !rejectRolesNotFound) { - LOG.debug("JWT validation is skipped due to 'intercept-method-patterns' configuration being empty"); - return forward(call, metadata, next); - } else if (requiredAccesses.isEmpty() && rejectRolesNotFound) { - throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT validation failed since no roles were found and 'reject-not-found' = true"); - } else if (requiredAccesses.isEmpty()) { // We don't need tp validate JWT - LOG.debug("JWT validation is skipped due to no matching 'intercept-method-patterns'"); - return forward(call, metadata, next); - } else if (requiredAccesses.contains(SecurityRule.DENY_ALL)) { - throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT validation failed since denyAll() requirement met"); - } else if (requiredAccesses.contains(SecurityRule.IS_ANONYMOUS)) { // We don't need tp validate JWT - LOG.debug("JWT validation is skipped since isAnonymous() requirement met"); - return forward(call, metadata, next); - } - if (!metadata.containsKey(jwtMetadataKey)) { - throw statusRuntimeException(config.getMissingTokenStatus(), "JWT validation failed since no JWT was found in metadata"); - } - final String token = metadata.get(jwtMetadataKey); - LOG.debug("JWT: {}", token); - final Optional jwtOptional = jwtValidator.validate(token, null); // We don't have an HttpRequest to send in here (hence null) - if (jwtOptional.isPresent()) { - if (requiredAccesses.contains(SecurityRule.IS_AUTHENTICATED)) { // Valid JWT is enough here - LOG.debug("JWT validation succeeded since isAuthenticated() requirement met"); - return forward(call, metadata, next); - } - return validateRoles(requiredAccesses, jwtOptional.get(), call, metadata, next); - } - throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT validation failed since no JWT was returned by validator"); - } - - /** - * Validate the JWT claims. - * - * @param requiredAccesses the list of required accesses - * @param jwt the JWT - * @param call the server call - * @param metadata the metadata - * @param next the next handler - * @param the type of the request - * @param the type of the response - * @return the server call listener - */ - private ServerCall.Listener validateRoles(final List requiredAccesses, final JWT jwt, - final ServerCall call, final Metadata metadata, final ServerCallHandler next) { - final List roles; - try { - roles = rolesFinder.findInClaims(new JwtClaimsSetAdapter(jwt.getJWTClaimsSet())); - } catch (final ParseException e) { - throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT validation failed due to parsing exception"); - } - if (rolesFinder.hasAnyRequiredRoles(requiredAccesses, roles)) { - LOG.debug("JWT validation succeeded with matching roles"); - return forward(call, metadata, next); - } else { - throw statusRuntimeException(config.getFailedValidationTokenStatus(), "JWT does not contain required roles"); - } - } - - /** - * Create a {@link StatusRuntimeException} for the given status code and message and log an error. - * - * @param statusCode the status code - * @param message the message to log an error with - * @return the status runtime exception - */ - private static StatusRuntimeException statusRuntimeException(final Status.Code statusCode, final String message) { - LOG.error(message); - return Status.fromCode(statusCode).withDescription("JWT validation failed").asRuntimeException(); - } - - /** - * Get the required access for the server call. - * - * @param serverCall the server call - * @param the request type - * @param the response type - * @return the required access - */ - private List getRequiredAccesses(final ServerCall serverCall) { - if (CollectionUtils.isEmpty(interceptMethodPatterns)) { - return Collections.emptyList(); - } - return interceptMethodPatterns.stream() - .filter(interceptMethodPattern -> serverCall.getMethodDescriptor().getFullMethodName().matches(interceptMethodPattern.getPattern())) - .map(InterceptUrlMapPattern::getAccess) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - } - - /** - * Forward the call to next handler. - * - * @param call the server call - * @param metadata the metadata - * @param next the next handler - * @param the type of the request - * @param the type of the response - * @return the server call listener - */ - private static ServerCall.Listener forward(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { - return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, metadata)) { }; - } - - /** - * Get the order for this interceptor within the interceptor chain. - * - * @return the order - */ - @Override - public int getOrder() { - return config.getInterceptorOrder(); - } - -} diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/JwtGrpcServerAuthenticationFetcherSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/JwtGrpcServerAuthenticationFetcherSpec.groovy new file mode 100644 index 000000000..914a2d3da --- /dev/null +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/JwtGrpcServerAuthenticationFetcherSpec.groovy @@ -0,0 +1,9 @@ +package io.micronaut.grpc.server.security.jwt + +import spock.lang.Specification + +class JwtGrpcServerAuthenticationFetcherSpec extends Specification { + + // TODO + +} diff --git a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy index b7a6a816a..e23e351ee 100644 --- a/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy +++ b/grpc-server-security-jwt/src/test/groovy/io/micronaut/grpc/server/security/jwt/interceptor/GrpcServerSecurityJwtInterceptorSpec.groovy @@ -28,7 +28,6 @@ class GrpcServerSecurityJwtInterceptorSpec extends Specification { private static final REQUIRED_ENV = "greeter-hello-world-jwt" private static final Map defaultConfigurations = [ - "micronaut.security.token.roles-name": true, "grpc.server.security.token.jwt.enabled": true, "micronaut.security.token.jwt.signatures.secret.generator.secret": "SeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3tSeCr3t", "micronaut.security.token.jwt.signatures.secret.generator.base64": false, diff --git a/grpc-server-security/build.gradle b/grpc-server-security/build.gradle new file mode 100644 index 000000000..1afa41f9d --- /dev/null +++ b/grpc-server-security/build.gradle @@ -0,0 +1,19 @@ + +dependencies { + + annotationProcessor "io.micronaut:micronaut-inject-java:$micronautVersion" + + api project(":grpc-server-runtime") + api "io.micronaut:micronaut-inject:$micronautVersion" + api "io.micronaut:micronaut-runtime:$micronautVersion" + + implementation("io.micronaut.security:micronaut-security:$micronautSecurityVersion") { + exclude group: 'io.micronaut', module: 'micronaut-http' + exclude group: 'io.micronaut', module: 'micronaut-http-server' + } + + testImplementation "io.micronaut:micronaut-inject-groovy:$micronautVersion" + testImplementation "io.micronaut:micronaut-inject-java:$micronautVersion" + testImplementation "io.micronaut.test:micronaut-test-spock:$micronautTestVersion" + +} diff --git a/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/AbstractGrpcServerSecurityRule.java b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/AbstractGrpcServerSecurityRule.java new file mode 100644 index 000000000..1cf33cbda --- /dev/null +++ b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/AbstractGrpcServerSecurityRule.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.grpc.server.security; + +import edu.umd.cs.findbugs.annotations.Nullable; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.security.rules.SecurityRuleResult; +import io.micronaut.security.token.MapClaims; +import io.micronaut.security.token.RolesFinder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Abstract gRPC server security rule. + * + * @since 2.4.0 + * @author Brian Wyka + */ +public abstract class AbstractGrpcServerSecurityRule implements GrpcServerSecurityRule { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractGrpcServerSecurityRule.class); + protected final RolesFinder rolesFinder; + + /** + * Constructs an instance of this rule with the provided configuration and roles finder. + * + * @param rolesFinder the roles finder + */ + protected AbstractGrpcServerSecurityRule(final RolesFinder rolesFinder) { + this.rolesFinder = rolesFinder; + } + + /** + * Appends {@link SecurityRule#IS_ANONYMOUS} if not authenticated. If the + * claims contain one or more roles, {@link SecurityRule#IS_AUTHENTICATED} is + * appended to the list. + * + * @param claims The claims of the token, null if not authenticated + * @return The granted roles + */ + protected List getRoles(@Nullable final Map claims) { + List roles = new ArrayList<>(); + if (claims == null) { + roles.add(SecurityRule.IS_ANONYMOUS); + } else { + if (!claims.isEmpty()) { + roles.addAll(rolesFinder.findInClaims(new MapClaims(claims))); + } + roles.add(SecurityRule.IS_ANONYMOUS); + roles.add(SecurityRule.IS_AUTHENTICATED); + } + return roles; + } + + /** + * Compares the given roles to determine if the request is allowed by + * comparing if any of the granted roles is in the required roles list. + * + * @param requiredRoles The list of roles required to be authorized + * @param grantedRoles The list of roles granted to the user + * @return {@link SecurityRuleResult#REJECTED} if none of the granted roles + * appears in the required roles list. {@link SecurityRuleResult#ALLOWED} otherwise. + */ + protected SecurityRuleResult compareRoles(final List requiredRoles, final List grantedRoles) { + if (rolesFinder.hasAnyRequiredRoles(requiredRoles, grantedRoles)) { + if (LOG.isDebugEnabled()) { + LOG.debug("The given roles [{}] matched one or more of the required roles [{}]. Allowing the request", grantedRoles, requiredRoles); + } + return SecurityRuleResult.ALLOWED; + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("None of the given roles [{}] matched the required roles [{}]. Rejecting the request", grantedRoles, requiredRoles); + } + return SecurityRuleResult.REJECTED; + } + } + +} diff --git a/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerAuthenticationFetcher.java b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerAuthenticationFetcher.java new file mode 100644 index 000000000..efc3c9216 --- /dev/null +++ b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerAuthenticationFetcher.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.grpc.server.security; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.micronaut.core.order.Ordered; +import io.micronaut.security.authentication.Authentication; + +import java.util.Optional; + +/** + * gRPC Authentication Fetcher. + * + * @since 2.4.0 + * @author Brian Wyka + */ +public interface GrpcServerAuthenticationFetcher extends Ordered { + + /** + * Attempts to read an {@link Authentication} from a {@link ServerCall} being executed. + * + * @param serverCall {@link ServerCall} being executed. + * @return {@link Authentication} if found + */ + Optional fetchAuthentication(final ServerCall serverCall, final Metadata metadata); + +} diff --git a/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityConfiguration.java b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityConfiguration.java new file mode 100644 index 000000000..b2c70b6a4 --- /dev/null +++ b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityConfiguration.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.grpc.server.security; + +import io.grpc.Status; +import io.micronaut.context.annotation.ConfigurationProperties; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.bind.annotation.Bindable; +import io.micronaut.core.util.Toggleable; +import io.micronaut.grpc.server.GrpcServerConfiguration; + +/** + * gRPC Server Security Configuration. + * + * @since 2.4.0 + * @author Brian Wyka + */ +@Requires(configuration = "io.micronaut.security") +@ConfigurationProperties(GrpcServerSecurityConfiguration.PREFIX) +public interface GrpcServerSecurityConfiguration extends Toggleable { + + String PREFIX = GrpcServerConfiguration.PREFIX + ".security"; + + /** + * The {@link Status} returned by the interceptor when there is no {@link io.micronaut.security.authentication.Authentication} present. + * The default value is {@link Status.Code#UNAUTHENTICATED} + * + * @return the status + */ + @Bindable(defaultValue = "UNAUTHENTICATED") + Status.Code getMissingAuthenticationStatus(); + + /** + * The {@link Status} returned by the interceptor when authorization fails. The + * default value is {@link Status.Code#PERMISSION_DENIED} + * + * @return the status + */ + @Bindable(defaultValue = "PERMISSION_DENIED") + Status.Code getFailedAuthorizationStatus(); + +} diff --git a/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityInterceptor.java b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityInterceptor.java new file mode 100644 index 000000000..e35b81a2f --- /dev/null +++ b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityInterceptor.java @@ -0,0 +1,186 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.grpc.server.security; + +import edu.umd.cs.findbugs.annotations.Nullable; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import io.micronaut.context.annotation.Requires; +import io.micronaut.security.authentication.Authentication; +import io.micronaut.security.config.SecurityConfiguration; +import io.micronaut.security.rules.SecurityRuleResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +/** + * gRPC Server Security Interceptor responsible for checking {@link GrpcServerSecurityRule} + * results prior to allowing access to gRPC server methods. + * + * @since 2.4.0 + * @author Brian Wyka + */ +@Singleton +@Requires(beans = GrpcServerSecurityConfiguration.class) +public class GrpcServerSecurityInterceptor implements ServerInterceptor { + + private static final Logger LOG = LoggerFactory.getLogger(GrpcServerSecurityInterceptor.class); + private final SecurityConfiguration securityConfiguration; + private final GrpcServerSecurityConfiguration grpcServerSecurityConfiguration; + private final Collection grpcServerAuthenticationFetchers; + private final Collection grpcServerSecurityRules; + + /** + * Constructs an instance of the security interceptor with the provided configurations and rules. + * @param securityConfiguration the security configuration + * @param grpcServerSecurityConfiguration the gRPC security configuration + * @param grpcServerAuthenticationFetchers the gRPC server authentication fetchers + * @param grpcServerSecurityRules the method security rules + */ + @Inject + public GrpcServerSecurityInterceptor(final SecurityConfiguration securityConfiguration, + final GrpcServerSecurityConfiguration grpcServerSecurityConfiguration, + final Collection grpcServerAuthenticationFetchers, + final Collection grpcServerSecurityRules) { + this.securityConfiguration = securityConfiguration; + this.grpcServerSecurityConfiguration = grpcServerSecurityConfiguration; + this.grpcServerAuthenticationFetchers = grpcServerAuthenticationFetchers; + this.grpcServerSecurityRules = grpcServerSecurityRules; + } + + /** + * Intercept the gRPC server call and check security rules. If no {@link Authentication} is found, the server call + * is forwarded onto the next server call handler. + * + * @param serverCall the server call + * @param metadata the metadata + * @param next the next server call handler + * @param the type of the request + * @param the type of the response + * @return the server call listener + */ + @Override + public ServerCall.Listener interceptCall(final ServerCall serverCall, final Metadata metadata, final ServerCallHandler next) { + final String fullMethodName = serverCall.getMethodDescriptor().getFullMethodName(); + final Optional> authentication = grpcServerAuthenticationFetchers.stream() + .map(grpcServerAuthenticationFetcher -> grpcServerAuthenticationFetcher.fetchAuthentication(serverCall, metadata)) + .findFirst(); + if (authentication.isPresent()) { + return checkRules(serverCall, metadata, next, fullMethodName, authentication.get().orElse(null)); + } + if (LOG.isDebugEnabled()) { + LOG.debug("No Authentication fetched for server call. {}.", fullMethodName); + } + return forward(serverCall, metadata, next); + } + + /** + * Check the rules to see if the server call should be allowed or rejected. + * + * @param serverCall the server call + * @param metadata the metadata + * @param next the next server call handler + * @param fullMethodName the full method name + * @param authentication the authentication (may be null) + * @param the type of the server call request + * @param the type of the server call response + * @return the server call listener + */ + private ServerCall.Listener checkRules(final ServerCall serverCall, final Metadata metadata, final ServerCallHandler next, + final String fullMethodName, @Nullable final Authentication authentication) { + final boolean forbidden = authentication != null; + final Optional> listenerOptional = grpcServerSecurityRules.stream() + .map(grpcServerSecurityRule -> checkRule(serverCall, metadata, next, authentication, fullMethodName, forbidden, grpcServerSecurityRule)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + if (listenerOptional.isPresent()) { + return listenerOptional.get(); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Authorized server call to {}. No rule provider authorized or rejected the call.", fullMethodName); + } + if (securityConfiguration.isRejectNotFound() && forbidden) { + throw Status.fromCode(grpcServerSecurityConfiguration.getFailedAuthorizationStatus()).asRuntimeException(); + } else if (securityConfiguration.isRejectNotFound()) { + throw Status.fromCode(grpcServerSecurityConfiguration.getMissingAuthenticationStatus()).asRuntimeException(); + } + return forward(serverCall, metadata, next); + } + + /** + * Check the rules to see if the server call should be allowed or rejected. + * + * @param serverCall the server call + * @param metadata the metadata + * @param next the next server call handler + * @param authentication the authentication + * @param fullMethodName the full method name + * @param forbidden true if there is no authentication + * @param grpcServerSecurityRule the method security rule to check + * @param the type of the server call request + * @param the type of the server call response + * @return the server call listener + */ + private Optional> checkRule(final ServerCall serverCall, final Metadata metadata, final ServerCallHandler next, + @Nullable final Authentication authentication, final String fullMethodName, + final boolean forbidden, final GrpcServerSecurityRule grpcServerSecurityRule) { + final Map claims = Optional.ofNullable(authentication) + .map(Authentication::getAttributes) + .orElse(null); + final SecurityRuleResult result = grpcServerSecurityRule.check(serverCall, metadata, claims); + if (result == SecurityRuleResult.REJECTED) { + if (LOG.isDebugEnabled()) { + LOG.debug("Unauthorized server call to {}. The rule provider {} rejected the call.", fullMethodName, grpcServerSecurityRule.getClass().getName()); + } + if (forbidden) { + throw Status.fromCode(grpcServerSecurityConfiguration.getFailedAuthorizationStatus()).asRuntimeException(); + } + throw Status.fromCode(grpcServerSecurityConfiguration.getMissingAuthenticationStatus()).asRuntimeException(); + } + if (result == SecurityRuleResult.ALLOWED) { + if (LOG.isDebugEnabled()) { + LOG.debug("Authorized server call to {}. The rule provider {} authorized the request.", fullMethodName, grpcServerSecurityRule.getClass().getName()); + } + return Optional.of(forward(serverCall, metadata, next)); + } + return Optional.empty(); + } + + /** + * Forward the call to next handler. + * + * @param call the server call + * @param metadata the metadata + * @param next the next handler + * @param the type of the request + * @param the type of the response + * @return the server call listener + */ + private static ServerCall.Listener forward(final ServerCall call, final Metadata metadata, final ServerCallHandler next) { + return new ForwardingServerCallListener.SimpleForwardingServerCallListener(next.startCall(call, metadata)) { }; + } + +} diff --git a/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityRule.java b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityRule.java new file mode 100644 index 000000000..ebefda9fa --- /dev/null +++ b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/GrpcServerSecurityRule.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.grpc.server.security; + +import edu.umd.cs.findbugs.annotations.Nullable; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.micronaut.core.order.Ordered; +import io.micronaut.security.rules.SecurityRuleResult; + +import java.util.Map; + +/** + * A security rule designed to offer gRPC server security. + * + * @since 2.4.0 + * @author Brian Wyka + */ +public interface GrpcServerSecurityRule extends Ordered { + + /** + * Check the server call and claims for access to the method. + * + * @param serverCall the server call + * @param metadata the metadata + * @param claims the claims + * @param the type of the server request + * @param the type of the server response + * @return the security rule result + */ + SecurityRuleResult check(final ServerCall serverCall, final Metadata metadata, @Nullable final Map claims); + +} diff --git a/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/InterceptUrlMapGrpcServerSecurityRule.java b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/InterceptUrlMapGrpcServerSecurityRule.java new file mode 100644 index 000000000..f16e5048a --- /dev/null +++ b/grpc-server-security/src/main/java/io/micronaut/grpc/server/security/InterceptUrlMapGrpcServerSecurityRule.java @@ -0,0 +1,86 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.grpc.server.security; + +import edu.umd.cs.findbugs.annotations.Nullable; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.micronaut.core.util.CollectionUtils; +import io.micronaut.security.config.InterceptUrlMapPattern; +import io.micronaut.security.config.SecurityConfiguration; +import io.micronaut.security.rules.SecurityRuleResult; +import io.micronaut.security.token.RolesFinder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Map; +import java.util.Optional; + +/** + * gRPC Method Security Rule which uses {@link SecurityConfiguration#getInterceptUrlMap()} to check rules. + * + * @since 2.4.0 + * @author Brian Wyka + */ +@Singleton +public class InterceptUrlMapGrpcServerSecurityRule extends AbstractGrpcServerSecurityRule { + + private static final Logger LOG = LoggerFactory.getLogger(InterceptUrlMapGrpcServerSecurityRule.class); + private final SecurityConfiguration securityConfiguration; + + /** + * Constructs an instance of this rule with the provided configuration and roles finder. + * + * @param securityConfiguration the security configuration + * @param rolesFinder the roles finder + */ + @Inject + public InterceptUrlMapGrpcServerSecurityRule(final SecurityConfiguration securityConfiguration, final RolesFinder rolesFinder) { + super(rolesFinder); + this.securityConfiguration = securityConfiguration; + } + + /** + * Run the security rule check. + * + * @param serverCall the server call + * @param metadata the metadata + * @param claims the claims + * @param the type of the server call request + * @param the type of the server call response + * @return the security rule result. + */ + @Override + public SecurityRuleResult check(final ServerCall serverCall, final Metadata metadata, @Nullable final Map claims) { + if (CollectionUtils.isEmpty(securityConfiguration.getInterceptUrlMap())) { + return SecurityRuleResult.UNKNOWN; + } + final String fullMethodName = serverCall.getMethodDescriptor().getFullMethodName(); + final Optional matchedPattern = securityConfiguration.getInterceptUrlMap().stream() + .filter(interceptUrlMapPattern -> serverCall.getMethodDescriptor().getFullMethodName().matches(interceptUrlMapPattern.getPattern())) + .findFirst(); + if (matchedPattern.isPresent()) { + return compareRoles(matchedPattern.get().getAccess(), getRoles(claims)); + } + if (LOG.isDebugEnabled()) { + LOG.debug("No intercept map patterns match for method [{}]. Returning UNKNOWN.", fullMethodName); + } + return SecurityRuleResult.UNKNOWN; + } + +} diff --git a/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/AbstractGrpcServerSecurityRuleSpec.groovy b/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/AbstractGrpcServerSecurityRuleSpec.groovy new file mode 100644 index 000000000..3389a8403 --- /dev/null +++ b/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/AbstractGrpcServerSecurityRuleSpec.groovy @@ -0,0 +1,9 @@ +package io.micronaut.grpc.server.security + +import spock.lang.Specification + +class AbstractGrpcServerSecurityRuleSpec extends Specification { + + // TODO + +} diff --git a/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/GrpcServerSecurityConfigurationSpec.groovy b/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/GrpcServerSecurityConfigurationSpec.groovy new file mode 100644 index 000000000..7a280c8f4 --- /dev/null +++ b/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/GrpcServerSecurityConfigurationSpec.groovy @@ -0,0 +1,9 @@ +package io.micronaut.grpc.server.security + +import spock.lang.Specification + +class GrpcServerSecurityConfigurationSpec extends Specification { + + // TODO + +} diff --git a/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/GrpcServerSecurityInterceptorSpec.groovy b/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/GrpcServerSecurityInterceptorSpec.groovy new file mode 100644 index 000000000..7cc7efc14 --- /dev/null +++ b/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/GrpcServerSecurityInterceptorSpec.groovy @@ -0,0 +1,9 @@ +package io.micronaut.grpc.server.security + +import spock.lang.Specification + +class GrpcServerSecurityInterceptorSpec extends Specification { + + // TODO + +} diff --git a/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/InterceptUrlMapGrpcServerSecurityRuleSpec.groovy b/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/InterceptUrlMapGrpcServerSecurityRuleSpec.groovy new file mode 100644 index 000000000..172cafa80 --- /dev/null +++ b/grpc-server-security/src/test/groovy/io/micronaut/grpc/server/security/InterceptUrlMapGrpcServerSecurityRuleSpec.groovy @@ -0,0 +1,9 @@ +package io.micronaut.grpc.server.security + +import spock.lang.Specification + +class InterceptUrlMapGrpcServerSecurityRuleSpec extends Specification { + + // TODO + +} diff --git a/grpc-server-security/src/test/resources/logback.xml b/grpc-server-security/src/test/resources/logback.xml new file mode 100644 index 000000000..aa04691ff --- /dev/null +++ b/grpc-server-security/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 88b6f44dc..a70e8a20b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,8 @@ rootProject.name = 'grpc' include 'grpc-annotation' include 'grpc-client-runtime' include 'grpc-server-runtime' +include 'grpc-server-security' include 'grpc-server-security-jwt' include 'grpc-runtime' -include 'protobuff-support' \ No newline at end of file +include 'protobuff-support' +