Intercept gRPC C++ calls in server and client

ghz 1years ago ⋅ 6206 views

Question

Basic task I want to do: Provide a Authenticate service in gRPC server that all clients call (and supply user name and password) initially to obtain a authorization token (say JWT). Next, when other service calls are made by the client, the token should be verified.

This can be accomplished in Java APIs easily using ServerInterceptor and ClientInterceptor interfaces. In ServerInterceptor I can check which service is called and decide whether to allow or deny the call. On the ClientInterceptor side I can add the authorization token as metadata to every service call.

There is this AuthMetadataProcessor abstract class in C++. But not sure how to accomplish the task similar to Java APIs. Is there a way to do similar things in C++ APIs ?


Answer

Yes. You need to subclass AuthMetadataProcessor, override its Process method and register an instance of the derived type with your service. Once that is done, all method invocations will be intercepted by Process and it will be given the client metadata sent with the request.

Your implementation of Process must decide whether authentication is required for the intercepted method (i.e., cannot be required for your Authenticate method, but will be required for various subsequently invoked methods). This can be done by examining the :path metadata key, as documented in issue #9211, which is a trusted value designating the intercepted method.

Your implementation of Process must decide whether the token is supplied in the request and is valid. This is an implementation detail, but generally Process refers to a store of valid tokens generated by Authenticate. Which is probably how you have it set up in Java already.

Unfortunately, one cannot register an AuthMetadataProcessor on top of insecure credentials, meaning that you will have to use SSL, or else attempt to intercept methods differently.

The framework also provides convenience functionality allowing you to work with a peer identity property. Process can call AddProperty on the authentication context, providing the identity implied by the token, followed by SetPeerIdentityPropertyName. The invoked method can then access the information using GetPeerIdentity and avoid remapping tokens to identities.

AuthMetadataProcessor Implementation Example

struct Const
{
    static const std::string& TokenKeyName() { static std::string _("token"); return _; }
    static const std::string& PeerIdentityPropertyName() { static std::string _("username"); return _; }    
};

class MyServiceAuthProcessor : public grpc::AuthMetadataProcessor
{

public:

    grpc::Status Process(const InputMetadata& auth_metadata, grpc::AuthContext* context, OutputMetadata* consumed_auth_metadata, OutputMetadata* response_metadata) override
    {
        // determine intercepted method
        std::string dispatch_keyname = ":path";
        auto dispatch_kv = auth_metadata.find(dispatch_keyname);
        if (dispatch_kv == auth_metadata.end())
            return grpc::Status(grpc::StatusCode::INTERNAL, "Internal Error");

        // if token metadata not necessary, return early, avoid token checking
        auto dispatch_value = std::string(dispatch_kv->second.data());
        if (dispatch_value == "/MyPackage.MyService/Authenticate")
            return grpc::Status::OK;

        // determine availability of token metadata
        auto token_kv = auth_metadata.find(Const::TokenKeyName());
        if (token_kv == auth_metadata.end())
            return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Missing Token");

        // determine validity of token metadata
        auto token_value = std::string(token_kv->second.data());
        if (tokens.count(token_value) == 0)
            return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Invalid Token");

        // once verified, mark as consumed and store user for later retrieval
        consumed_auth_metadata->insert(std::make_pair(Const::TokenKeyName(), token_value));     // required
        context->AddProperty(Const::PeerIdentityPropertyName(), tokens[token_value]);           // optional
        context->SetPeerIdentityPropertyName(Const::PeerIdentityPropertyName());                // optional

        return grpc::Status::OK;
    }

    std::map<std::string, std::string> tokens;
};

AuthMetadataProcessor Setup within Secure Service

class MyServiceImplSecure : public MyPackage::MyService::Service
{

public:

    MyServiceImplSecure(std::string _server_priv, std::string _server_cert, std::string _ca_cert) :
        server_priv(_server_priv), server_cert(_server_cert), ca_cert(_ca_cert) {}

    std::shared_ptr<grpc::ServerCredentials> GetServerCredentials()
    {
        grpc::SslServerCredentialsOptions::PemKeyCertPair pkcp;
        pkcp.private_key = server_priv;
        pkcp.cert_chain = server_cert;

        grpc::SslServerCredentialsOptions ssl_opts;
        ssl_opts.pem_key_cert_pairs.push_back(pkcp);
        ssl_opts.pem_root_certs = ca_cert;

        std::shared_ptr<grpc::ServerCredentials> creds = grpc::SslServerCredentials(ssl_opts);
        creds->SetAuthMetadataProcessor(auth_processor);
        return creds;
    }

    void GetContextUserMapping(::grpc::ServerContext* context, std::string& username)
    {
        username = context->auth_context()->GetPeerIdentity()[0].data();
    }

private:

    std::string server_priv;
    std::string server_cert;
    std::string ca_cert;

    std::shared_ptr<MyServiceAuthProcessor> auth_processor =
        std::shared_ptr<MyServiceAuthProcessor>(new MyServiceAuthProcessor());
};