Skip to content

Commit

Permalink
gRPC JWT-based authentication (#258)
Browse files Browse the repository at this point in the history
* GRPC jwt based authentication

* cleanup deps

* integration test

* fix side-effect of default version change

* fix side-effect of default version change

* Update plugins/grpc-transport-auth/build.gradle

Co-Authored-By: Sergei Egorov <bsideup@gmail.com>

* reuse tck app runner to keep e2e test in module

* explicit dep on mem plugins

* cleanups

* pin the gRPC version and add the issue link

Co-authored-by: Sergei Egorov <bsideup@gmail.com>
  • Loading branch information
lanwen and bsideup authored Feb 13, 2020
1 parent 99d1c88 commit dd1d42e
Show file tree
Hide file tree
Showing 9 changed files with 637 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@

import lombok.Getter;
import lombok.NonNull;
import org.pf4j.DefaultPluginManager;
import org.pf4j.ExtensionFinder;
import org.pf4j.ManifestPluginDescriptorFinder;
import org.pf4j.PluginDescriptorFinder;
import org.pf4j.PluginLoader;
import org.pf4j.PluginRepository;
import lombok.experimental.Delegate;
import org.pf4j.*;

import java.nio.file.Path;

Expand All @@ -21,6 +17,24 @@ public LiiklusPluginManager(@NonNull Path pluginsRoot, @NonNull String pluginsPa
this.pluginsPathMatcher = pluginsPathMatcher;
}

@Override
protected VersionManager createVersionManager() {
var versionManager = super.createVersionManager();

class DelegatingVersionManager implements VersionManager {
@Delegate
final VersionManager delegate = versionManager;
}

return new DelegatingVersionManager() {
@Override
public boolean checkVersionConstraint(String version, String constraint) {
// TODO https://github.com/pf4j/pf4j/issues/367
return "*".equals(constraint) || super.checkVersionConstraint(version, constraint);
}
};
}

@Override
protected PluginDescriptorFinder createPluginDescriptorFinder() {
return new ManifestPluginDescriptorFinder();
Expand Down
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ configure(subprojects.findAll { !it.name.startsWith("examples/") }) {
mavenBom 'org.junit:junit-bom:5.5.2'
mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
mavenBom 'org.testcontainers:testcontainers-bom:1.12.3'
mavenBom 'io.grpc:grpc-bom:1.24.1'

// pinned to 1.23.1, see https://github.com/grpc/grpc-java/issues/6707
mavenBom 'io.grpc:grpc-bom:1.23.1'
mavenBom 'com.google.protobuf:protobuf-bom:3.10.0'
}

Expand Down Expand Up @@ -109,6 +111,8 @@ configure(subprojects.findAll { !it.name.startsWith("examples/") }) {
dependency 'org.apache.kafka:kafka-clients:2.3.1'

dependency 'com.salesforce.servicelibs:reactor-grpc-stub:0.10.0'
dependency 'com.avast.grpc.jwt:grpc-java-jwt:0.2.0'
dependency 'com.auth0:java-jwt:3.9.0'

dependency 'org.awaitility:awaitility:4.0.1'
}
Expand Down
42 changes: 42 additions & 0 deletions plugins/grpc-transport-auth/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
jar {
manifest {
attributes(
'Plugin-Id': "${project.name}",
'Plugin-Version': "${project.version}",
'Plugin-Dependencies': [
project(":grpc-transport").name
].join(","),
)
}

into('lib') {
from(configurations.compile - configurations.compileOnly)
}
}

tasks.test.dependsOn(jar)
tasks.test.dependsOn(
[":inmemory-records-storage", ":inmemory-positions-storage", ":grpc-transport"].collect {
project(it).getTasksByName("jar", true)
}
)

dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
compileOnly 'com.google.auto.service:auto-service'
annotationProcessor 'com.google.auto.service:auto-service'

compileOnly project(":app")
compileOnly project(":grpc-transport")

compile 'com.auth0:java-jwt'
compile 'com.avast.grpc.jwt:grpc-java-jwt'

testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testCompile project(":tck")
testCompile project(":client")
testCompile 'org.springframework.boot:spring-boot-starter-test'
testRuntime project(":grpc-transport")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.github.bsideup.liiklus.transport.grpc;

import com.auth0.jwt.interfaces.RSAKeyProvider;
import lombok.SneakyThrows;

import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;

public class StaticRSAKeyProvider implements RSAKeyProvider {
private Map<String, RSAPublicKey> keys;

public StaticRSAKeyProvider(Map<String, String> keys) {
this.keys = keys.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
key -> {
try {
return parsePubKey(key.getValue());
} catch (InvalidKeySpecException e) {
throw new IllegalArgumentException(String.format("Invalid RSA pubkey with id %s", key.getKey()), e);
}
}
));
}

@Override
public RSAPublicKey getPublicKeyById(String keyId) {
if (!keys.containsKey(keyId)) {
throw new NoSuchElementException(String.format("KeyId %s is not defined to authorize GRPC requests", keyId));
}
return keys.get(keyId);
}

@Override
public RSAPrivateKey getPrivateKey() {
return null; // we don't sign anything
}

@Override
public String getPrivateKeyId() {
return null; // we don't sign anything
}

/**
* Standard "ssh-rsa AAAAB3Nza..." pubkey representation could be converted to a proper format with
* `ssh-keygen -f id_rsa.pub -e -m pkcs8`
*
* This method will work the same if you strip beginning, as well as line breaks on your own
*
* @param key X509 encoded (with -----BEGIN PUBLIC KEY----- lines)
* @return parsed string
*/
@SneakyThrows(NoSuchAlgorithmException.class)
static RSAPublicKey parsePubKey(String key) throws InvalidKeySpecException {
String keyContent = key.replaceAll("\\n", "")
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "");

byte[] byteKey = Base64.getDecoder().decode(keyContent);
var x509EncodedKeySpec = new X509EncodedKeySpec(byteKey);

return (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.github.bsideup.liiklus.transport.grpc.config;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.avast.grpc.jwt.server.JwtServerInterceptor;
import com.github.bsideup.liiklus.transport.grpc.GRPCLiiklusTransportConfigurer;
import com.github.bsideup.liiklus.transport.grpc.StaticRSAKeyProvider;
import com.github.bsideup.liiklus.util.PropertiesUtil;
import com.google.auto.service.AutoService;
import io.grpc.netty.NettyServerBuilder;
import lombok.Data;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.group.GroupSequenceProvider;
import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;

import javax.validation.constraints.NotEmpty;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
@AutoService(ApplicationContextInitializer.class)
public class GRPCAuthConfig implements ApplicationContextInitializer<GenericApplicationContext> {

@Override
public void initialize(GenericApplicationContext applicationContext) {
var environment = applicationContext.getEnvironment();

var authProperties = PropertiesUtil.bind(environment, new GRPCAuthProperties());

if (authProperties.getAlg() == GRPCAuthProperties.Alg.NONE) {
return;
}

log.info("GRPC Authorization ENABLED with algorithm {}", authProperties.getAlg());

// Init it early to check that everything is fine in config
JWTVerifier verifier = createVerifier(authProperties.getAlg(), authProperties);

applicationContext.registerBean(
JWTAuthGRPCTransportConfigurer.class,
() -> new JWTAuthGRPCTransportConfigurer(verifier)
);
}

private JWTVerifier createVerifier(GRPCAuthProperties.Alg alg, GRPCAuthProperties properties) {
switch (alg) {
case HMAC512:
return JWT
.require(Algorithm.HMAC512(properties.getSecret()))
.acceptLeeway(2)
.build();
case RSA512:
return JWT
.require(Algorithm.RSA512(new StaticRSAKeyProvider(properties.getKeys())))
.acceptLeeway(2)
.build();
default:
throw new IllegalStateException("Unsupported algorithm");
}
}

@Value
static class JWTAuthGRPCTransportConfigurer implements GRPCLiiklusTransportConfigurer {
private JWTVerifier verifier;

@Override
public void apply(NettyServerBuilder builder) {
builder.intercept(new JwtServerInterceptor<>(verifier::verify));
}
}

@ConfigurationProperties("grpc.auth")
@Data
@GroupSequenceProvider(GRPCAuthProperties.EnabledSequenceProvider.class)
static class GRPCAuthProperties {

Alg alg = Alg.NONE;

@NotEmpty(groups = Symmetric.class)
String secret;

@NotEmpty(groups = Asymmetric.class)
Map<String, String> keys = Map.of();

enum Alg {
NONE,
RSA512,
HMAC512,
}

interface Symmetric {
}

interface Asymmetric {
}

public static class EnabledSequenceProvider implements DefaultGroupSequenceProvider<GRPCAuthProperties> {

@Override
public List<Class<?>> getValidationGroups(GRPCAuthProperties object) {
var sequence = new ArrayList<Class<?>>();
sequence.add(GRPCAuthProperties.class);
if (object != null && object.getAlg() == Alg.HMAC512) {
sequence.add(Symmetric.class);
}
if (object != null && object.getAlg() == Alg.RSA512) {
sequence.add(Asymmetric.class);
}
return sequence;
}
}
}
}
Loading

0 comments on commit dd1d42e

Please sign in to comment.