Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature Request: Server Side JWT Security Support #164

Open
brianwyka opened this issue Jul 9, 2020 · 28 comments · May be fixed by #322
Open

Feature Request: Server Side JWT Security Support #164

brianwyka opened this issue Jul 9, 2020 · 28 comments · May be fixed by #322
Assignees
Labels
type: enhancement New feature or request

Comments

@brianwyka
Copy link
Contributor

brianwyka commented Jul 9, 2020

Allow for integration with micronaut-security to provide server interceptors to enforce different types of authentication

Base use case would be micronaut-security-jwt to be used for validating JWT tokens. Example below:

package com.example;

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.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.micronaut.context.annotation.Value;
import lombok.extern.slf4j.Slf4j;
import lombok.val;

import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.security.Key;
import java.util.Base64;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@Singleton
public class JwtServerInterceptor implements ServerInterceptor {

    private static final Metadata.Key<String> JWT_HEADER_KEY = Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER);
    private final Set<Key> jwtSignatureKeys;

    /**
     * Create the interceptor with a set of all the provided signature keys
     *
     * @param jwtServerSignatureKeyCsv a CSV string of all the base64 encoded signature keys
     */
    @Inject
    public JwtServerInterceptor(@Value("${jwt.server.signature.key}") final String jwtServerSignatureKeyCsv) {
        this.jwtSignatureKeys = Stream.of(jwtServerSignatureKeyCsv.split(","))
                .map(Base64.getDecoder()::decode)
                .map(keyBytes -> new SecretKeySpec(keyBytes, SignatureAlgorithm.HS512.getJcaName()))
                .collect(Collectors.toSet());
    }

    /** {@inheritDoc} */
    @Override
    public <T, S> ServerCall.Listener<T> interceptCall(final ServerCall<T, S> call, final Metadata headers, final ServerCallHandler<T, S> next) {
        if (!headers.containsKey(JWT_HEADER_KEY)) {
            log.error("JWT token missing in gRPC headers");
            throw new StatusRuntimeException(Status.UNAUTHENTICATED);
        }
        val listener = next.startCall(call, headers);
        try {
            val jwt = jwtSignatureKeys.stream()
                    .map(Jwts.parser()::setSigningKey)
                    .map(parser -> parser.parse(headers.get(JWT_HEADER_KEY)))
                    .findAny()
                    .orElseThrow(Status.PERMISSION_DENIED.withDescription("Unable to validate JWT with any signature keys")::asRuntimeException);
            if (log.isDebugEnabled()) {
                log.debug("JWT: {}", jwt);
            }
        } catch (final Exception e) {
            throw Status.PERMISSION_DENIED.withCause(e).asRuntimeException();
        }
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<T>(listener) { };
    }

}
@brianwyka
Copy link
Contributor Author

Worth adding as a new module micronaut-security-jwt with a grpc flavor?

@graemerocher
Copy link
Contributor

Sounds like a good idea, want to do a PR?

@brianwyka
Copy link
Contributor Author

brianwyka commented Jul 9, 2020

Sure, I can take a stab at it. Is it better suited to live in this project or within https://github.com/micronaut-projects/micronaut-security ?

@graemerocher
Copy link
Contributor

this project is fine

@graemerocher graemerocher added the type: enhancement New feature or request label Jul 18, 2020
@graemerocher
Copy link
Contributor

@brianwyka did you still want to submit a PR?

@brianwyka
Copy link
Contributor Author

Sorry @graemerocher, totally lost track of this. I can get something going this weekend.

@brianwyka
Copy link
Contributor Author

brianwyka commented Feb 13, 2021

@graemerocher, I was planning on reusing some of the JWT security configuration and beans from micronaut-security-jwt but there seems to be some ties to HttpRequest in there, and the only methods I can call without the request are @deprecated.

https://micronaut-projects.github.io/micronaut-security/latest/api/io/micronaut/security/token/jwt/validator/JwtValidator.html#validate-com.nimbusds.jwt.JWT-

Should I copy code into this new module or call the deprecated code?

Here is what my "basic" first take of an implementation looks like:

Configuration class:

@ConfigurationProperties(GrpcServerSecurityJwtConfiguration.PREFIX)
@Requires(property = GrpcServerSecurityJwtConfiguration.PREFIX + ".enabled", value = "true", defaultValue = "false")
public interface GrpcServerSecurityJwtConfiguration {

    String PREFIX = GrpcServerConfiguration.PREFIX + ".security.jwt";

    /**
     * Whether or not JWT server interceptor is enabled
     *
     * @return true if enabled, false otherwise
     */
    boolean isEnabled();

    /**
     * The order to be applied to the server interceptor in the interceptor chain
     *
     * @return the order
     */
    int getOrder();

    /**
     * The name of the metadata key which holds the JWT
     *
     * @return the metadata key name
     */
    String getMetadataKeyName();

}

Server Interceptor:

@Singleton
@Requires(beans = GrpcServerSecurityJwtConfiguration.class)
public class GrpcServerSecurityJwtInterceptor implements ServerInterceptor, Ordered {

    private static final Logger LOG = LoggerFactory.getLogger(GrpcServerSecurityJwtInterceptor.class);

    private final int order;
    private final Metadata.Key<String> jwtMetadataKey;
    private final JwtValidator jwtValidator;

    /**
     * Create the interceptor based on the configuration.
     *
     * @param config the gRPC Security JWT configuration
     * @param jwtValidator the JWT validator
     */
    @Inject
    public GrpcServerSecurityJwtInterceptor(final GrpcServerSecurityJwtConfiguration config, final JwtValidator jwtValidator) {
        this.order = config.getOrder();
        this.jwtMetadataKey = Metadata.Key.of(config.getMetadataKeyName(), Metadata.ASCII_STRING_MARSHALLER);
        this.jwtValidator = jwtValidator;
    }

    /**
     * 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 <T> the type of the server request
     * @param <S> the type of the server response
     * @throws StatusRuntimeException if token not present or invalid
     */
    @Override
    public <T, S> ServerCall.Listener<T> interceptCall(final ServerCall<T, S> call, final Metadata metadata, final ServerCallHandler<T, S> next) {
        if (!metadata.containsKey(jwtMetadataKey)) {
            if (LOG.isErrorEnabled()) {
                LOG.error("{} key missing in gRPC metadata", jwtMetadataKey.name());
            }
            throw new StatusRuntimeException(Status.UNAUTHENTICATED);
        }
        final ServerCall.Listener<T> listener = next.startCall(call, metadata);
        final Optional<JWT> jwtOptional = jwtValidator.validate(metadata.get(jwtMetadataKey));
        if (!jwtOptional.isPresent()) {
            throw Status.PERMISSION_DENIED.withDescription("JWT validation failed").asRuntimeException();
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("JWT: {}", jwtOptional.get().serialize());
        }
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<T>(listener) { };
    }

    /**
     * Get the order for this interceptor within the interceptor chain
     * 
     * @return the order
     */
    @Override
    public int getOrder() {
        return order;
    }

}

@graemerocher
Copy link
Contributor

@jameskleeh @sdelamo can you provide some suggestions with regards to integrating security with gRPC here?

@brianwyka
Copy link
Contributor Author

@jameskleeh, @sdelamo, any feedback?

@jameskleeh
Copy link
Contributor

@brianwyka As you have discovered many of the APIs in security are tied to the notion of an HttpRequest. If the same sort of practices for normal http services also apply to GRPC then perhaps I would consider creating a GrpcRequest object that implements HttpRequest to pass around. I would not rely on the deprecated APIs as they will be removed in a future release.

@rafaelsalleszup
Copy link

any news? we are in need of this functionality.

@brianwyka
Copy link
Contributor Author

Thanks for the feedback @jameskleeh, I'll see if I can make that work, otherwise will resort to some duplication if necessary.

@brianwyka
Copy link
Contributor Author

brianwyka commented Feb 22, 2021

@jameskleeh, based on some brief analysis, doesn't look like a GrpcRequest implementation of HttpRequest will be a viable or straight-forward option.

I noticed that HttpRequest is @Nullable at the moment. Is that temporary until the deprecation is removed?
https://github.com/micronaut-projects/micronaut-security/blob/master/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JwtValidator.java#L81

Or can I leverage calling that with a null request here...?

I'm thinking that perhaps an enhancement can be made to micronaut-security to add more of a limited scope object to the validate method of the JwtValidator. What is planned to be used off of the HttpRequest in the future? Currently I see the trace of HttpRequest die here in the JwtClaimsValidator:
https://github.com/micronaut-projects/micronaut-security/blob/master/security-jwt/src/main/java/io/micronaut/security/token/jwt/validator/JwtClaimsValidator.java#L42

If we want to make it more useful for other security purposes outside of HttpRequest realm, it would be beneficial to make this enhancement.

@jameskleeh
Copy link
Contributor

I noticed that HttpRequest is @nullable at the moment. Is that temporary until the deprecation is removed?

That isn't temporary. We want to allow for validation of JWTs outside of a request. You can pass null there. Some of the cases where the request is available are because users requested access to it, not because the framework itself needed the data

@brianwyka
Copy link
Contributor Author

Thanks @jameskleeh, that will work for this use case.

@brianwyka
Copy link
Contributor Author

#322 has been opened to support JWT server-side security

@brianwyka brianwyka changed the title Feature Request: Server Side Security Support Feature Request: Server Side JWT Security Support Feb 22, 2021
@pfyod
Copy link

pfyod commented Mar 3, 2021

I believe that a pretty crucial part is missing here for this to be on par with the "HTTP security" module: token propagation. Ideally, it should be possible to configure HTTP → gRPC, gRPC → HTTP and gRPC → gRPC token propagation.

@graemerocher
Copy link
Contributor

@pfyod that would be useful

@brianwyka
Copy link
Contributor Author

I agree it would be nice. Probably better to do in a follow up feature. @pfyod are you interested in working on it once #322 is complete?

@brunorafaeli
Copy link

Do we have news about this feature? 👀

@brianwyka
Copy link
Contributor Author

@brunorafaeli, still waiting on feedback on some design direction from Micronaut team.

@FrogDevelopper
Copy link

Any update on it would be great

@burtbeckwith
Copy link
Member

@FrogDevelopper I'll be looking at this later this week

@FrogDevelopper
Copy link

Hello,

happy new year for all the team 🎉 🍾

Hoping you'll get time for this feature soon🤞🏻

@FrogDevelopper
Copy link

@burtbeckwith
Still no time to work on it ?

@mariocards
Copy link

@burtbeckwith any update?

@segocago
Copy link

Any update on the issue?

@FrogDevelopper
Copy link

Still no update on this issue ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement New feature or request
Projects
None yet
10 participants