Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

GRPC jwt based call authentication #258

Merged
merged 11 commits into from
Feb 13, 2020
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,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.8.1'

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

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

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")
compile project(":grpc-transport")
compile 'com.auth0:java-jwt'
compile 'com.avast.grpc.jwt:grpc-java-jwt'

compileOnly project(":api")

testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testCompile 'org.junit.jupiter:junit-jupiter-engine'
testCompile 'org.junit.jupiter:junit-jupiter-params'
testCompile project(":app")
testCompile project(":testing")
testCompile project(":client")
testCompile 'org.springframework.boot:spring-boot-starter-test'
testCompile 'org.springframework.boot:spring-boot-starter-validation'
testCompile 'com.fasterxml.jackson.core:jackson-databind'

}
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,106 @@
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 lombok.Data;
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());

JWTVerifier verifier = createVerifier(authProperties.getAlg(), authProperties);

applicationContext.registerBean(
GRPCLiiklusTransportConfigurer.class,
() -> builder -> builder.intercept(new JwtServerInterceptor<>(verifier::verify))
);
}

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");
}
}

@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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.github.bsideup.liiklus.transport.grpc;


import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.security.spec.InvalidKeySpecException;
import java.util.Map;
import java.util.NoSuchElementException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class StaticRSAKeyProviderTest {

private static final String STRIPPED_4096 = "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApkuZHC50VVyc6mkqWMXl" +
"+fuhBmXhw8N8w6A0mlxRDHltdsPYvxE5n/Id4xUDCISZfjXIuSVyq/a7K3esbEqc" +
"fs6/dm6PQuLMsaxEzS3Gxn+QxELJ41IyKiT0DFAhorSoFChfzkkS7whHm+O8wVDI" +
"Z0Aj5TjY5t0/1CvU7wKeMDjVqOR3usEb37/5qu4ps0RbgQzBKjoJ3LSo/tt4tZw+" +
"V3dT2lEVCKCA9OA0I5UXFUwUyMH8NudSlEpExGcmNHM4sEW4NK4Y7RW9tyDT0RQR" +
"ydUIP8rXkjqyxMyHnwNUuzxJHqIXAdEhzw2xGLBSxr87wfmK09TEfSjmMemHfCfF" +
"Ht+esDSy7zRB68hCS/chyN57xyBWG3BeaKeJm34gLU6gt+9Bhvq90a0RXA7TXK7y" +
"QwhDQQwNPhUQshE036l/jCDxmgJZPNkvpweAeROsoEDf5o0TRaybXbyQh+jn+iJP" +
"ve7K2bTixmjlQKOWB4HZ+1YWyTzUabpdeuHVokKuVFzpKqi5oid3Bz17XU4fN36e" +
"M9CSV1urnlgdVwKwYttFwuerstwpB2rOT1UmamQhPwfDGy9x2d2vghSi+ELzKkKv" +
"yAlkIdeK/WLIi3l/R4pCFC1JfAGagXS+Jtvr9+PkiD3bG220HpW1ry68CZcsO91z" +
"7UCJcQMxXdt1gk3K+EbWaDUCAwEAAQ==";

private static final String FULL_2048 = "-----BEGIN PUBLIC KEY-----\n" +
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6b/6nQLIQQ8fHT4PcSyb\n" +
"hOLUE/237dgicbjsE7/Z/uPffuc36NTMJ122ppz6dWYnCrQ6CeTgAde4hlLE7Kvv\n" +
"aFiUbe5XKwSL8KV292XqrwRZhMI58TTTygcrBodYGzHy0Yytv703rz+9Qt5HO5BF\n" +
"02/+sM+Z0wlH6aXl3K3/2HfSOfitqnArBGaAs+PRNX2jlVKD1c9Cb7vo5L0X7q+6\n" +
"55uBErEoN7IHbj1u33qI/xEvPSycIiT2RXMGZkvDZH6mTsALel4aP4Qpp1NcE+kD\n" +
"itoBYAPTGgR4gBQveXZmD10yUVgJl2icINY3FvT9oJB6wgCY9+iTvufPppT1RPFH\n" +
"dQIDAQAB\n" +
"-----END PUBLIC KEY-----\n";

@ParameterizedTest
@ValueSource(strings = {
STRIPPED_4096,
FULL_2048
})
void shouldParseX509(String pubkey) throws InvalidKeySpecException {
var parsed = StaticRSAKeyProvider.parsePubKey(pubkey);
assertThat(parsed).isNotNull();
assertThat(parsed.getAlgorithm()).isEqualTo("RSA");
}

@Test
void shouldThrowExceptionOnInvalid() {
assertThatThrownBy(() -> StaticRSAKeyProvider.parsePubKey("")).isInstanceOf(InvalidKeySpecException.class);
}

@Test
void shouldCreateProviderInstance() {
new StaticRSAKeyProvider(Map.of(
"valid", STRIPPED_4096
));
}

@Test
void shouldHandleValidAndInvalidWithExceptionInConstructor() {
assertThatThrownBy(() -> new StaticRSAKeyProvider(Map.of(
"valid", STRIPPED_4096,
"invalid", ""
)))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void shouldComplainOnNotFoundKey() {
assertThatThrownBy(() -> new StaticRSAKeyProvider(Map.of(
"valid", STRIPPED_4096
)).getPublicKeyById("unknown"))
.isInstanceOf(NoSuchElementException.class);
}
}
Loading