-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
gRPC JWT-based authentication (#258)
* 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
Showing
9 changed files
with
637 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
73 changes: 73 additions & 0 deletions
73
...rt-auth/src/main/java/com/github/bsideup/liiklus/transport/grpc/StaticRSAKeyProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
119 changes: 119 additions & 0 deletions
119
...t-auth/src/main/java/com/github/bsideup/liiklus/transport/grpc/config/GRPCAuthConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.