Grpc-web: Documentation for Credentials

Created on 5 Nov 2018  路  13Comments  路  Source: grpc/grpc-web

We would like to be able to pass in CallCredentials to Grpc-web requests. I see that the Client objects support taking a credentials argument:

` /**

  • @private @const {?Object} The credentials to be used to connect
  • to the server
    */
    this.credentials_ = credentials;`

But I don't see that this is used anywhere. We're looking to add in a metadata Authorization token automatically on every request for example.

What is the proper way to implement this?

Most helpful comment

Allowing sending cookies is tracked at #176
But yes, an even better design is Interceptor support, which is tracked at #283.
Both are very much in our radar.

All 13 comments

You can add your Authorization token as a key:value in the metadata parameter per generated RPC method (usually the 2nd parameter).

The credentials object in the client constructor is currently not hooked up yet.

We are passing it now this way, but it requires remembering to do this per each call of the RPC. Is there a timeline for the credentials capability?

We have also considered another option, of simply sending cookies with each request and authenticating that way. Would this be reasonable?

Automatically attaching a header on outgoing requests is a typical use of a gRPC client interceptor (like http middleware). I think interceptor support would help solve this issue.

Allowing sending cookies is tracked at #176
But yes, an even better design is Interceptor support, which is tracked at #283.
Both are very much in our radar.

Thank you Stanley, our solution for today is to structure the service on the same origin, so we can send cookies with the request automatically.

@jakepoz could you elaborate how to send the cookies with request direct without add metadata per rpc call? Thanks

Yes, our general stack is a React Frontend (made with Create-React-App) and a GRPC backend. We setup a reverse proxy (check out Envoy for example) so that the GRPC servicers are served from the same origin as the frontend, but with a certain prefix (In our example, / serves the frontend, and /grpc is the prefix for all GRPC servicers)

Then, when making your promise client you can specify a prefix in the URL.
var BillingService = new BillingPromiseClient('/grpc');

Once you set your cookies (either though some frontend request, or manually via Javascript), then they will be sent automatically on all requests to the same origin.

that is smart! thanks.

in case anyone have similar problem. I finally put together a enovy proxy config that works.

```{yaml}

admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }

static_resources:
listeners:

  • name: listener_0
    address:
    socket_address: { address: 0.0.0.0, port_value: 8080 }
    filter_chains:

    • filters:



      • name: envoy.http_connection_manager


        config:


        codec_type: auto


        stat_prefix: ingress_http


        access_log:





        • name: envoy.file_access_log



          config:



          path: "/tmp/access.log"



          route_config:



          name: local_route



          virtual_hosts:



        • name: local_service



          domains: ["*"]



          routes:







          • match: { prefix: "/admin.Admin" }




            route:




            cluster: grpc_service




            max_grpc_timeout: 0s




          • match: { prefix: "/" }




            route:




            cluster: frontend




            http_filters:




            http_filters:







        • name: envoy.grpc_web



        • name: envoy.cors



        • name: envoy.router



          clusters:






  • name: grpc_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    hosts: [{ socket_address: { address: docker.for.mac.localhost, port_value: 9090 }}]
  • name: frontend
    connect_timeout: 2s
    type: logical_dns
    # the dns_lookup_family is required
    dns_lookup_family: V4_ONLY
    lb_policy: round_robin
    hosts: [{ socket_address: { address: docker.for.mac.localhost, port_value: 9092 }}]
    ```

PS: envoy is great, but the config is hard to get it right. (probably because I am too eager to get it right in a short time frame.)

For those who want to add the authorization header to each request, until interceptors are implemented, maybe you want to use my workaround:

(in typescript)

import { HelloWorldServicePromiseClient } from '....';

export class GrpcClient {
  public readonly hello: HelloWorldServicePromiseClient;

  constructor() {
    this.hello = this.mapMetadata(new HelloWorldServicePromiseClient('/api', null, null));
  }

  private mapMetadata<TClient>(client: TClient): TClient {
    for (const prop in client) {
      if (typeof client[prop] !== 'function') {
        continue;
      }

      const original = client[prop] as unknown as Function;
      client[prop] = ((...args: any[]) => {
        args[1] = {
          ...args[1],
          Authorization: `YAY MY TOKEN!`,
        };
        return original.call(client, ...args);
      }) as any;
    }
    return client;
  }
}

The basic idea is to create a general GrpcClient class that contains all services (for convenience). In this class, one can map all the function calls to add the authorization header to the metadata argument and then call the original function.

I know it seems kinda hacky, but at least for now (until interceptors are a thing) it works like a charm :-)

@buehler thanks for this -- I tweaked it so that you can instantiate a class for each grpc service individually and retain types:

// url to your service
CONST GRPC_URL = ''; // url to your service

export default class AuthorizedGrpcClient<T> {
  public readonly client: T

  // client takes 3 arguments to instantiate
  constructor(client: { new (arg1: string, arg2: any, arg3: any): T }) {
    const c = new client('my_service_url', null, null)
    this.client = this.mapMetadata(c)
  }

  // Intercepts all requests to methods on the grpc client and
  // adds sitewide headers to the second argument.
  //
  // This way, you don't need to pass those headers in for every single request. :)
  //
  private mapMetadata<TClient>(client: TClient): TClient {
    for (const prop in client) {
      if (typeof client[prop] !== 'function') {
        continue
      }

      const original = (client[prop] as unknown) as Function
      client[prop] = ((...args: any[]) => {
        args[1] = {
          ...args[1],
          Authorization: `YAY MY TOKEN!`,
        }
        return original.call(client, ...args)
      }) as any
    }
    return client
  }
}

and I use it like so:

const clientOne = new AuthorizedGrpcClient(FirstPromiseClient)
const clientTwo = new AuthorizedGrpcClient(SecondPromiseClient)

In reference to the Envoy config that @swuecho posted, here is a configuration that I used to have static content and the grpc server both handled at the root path. I was having an issue with the grpc server not accepting a dedicated "/grpc" path correctly.

With this setup I can easily have browser cookies set on localhost:8080 and read them as metadata on the grpc server. I also don't need to setup cors for this to work.

static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 8080 }
      filter_chains:
        - filters:
            - name: envoy.http_connection_manager
              config:
                codec_type: auto
                stat_prefix: ingress_http
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                            headers:
                              - name: "Content-Type"
                                exactMatch: "application/grpc-web-text"
                          route:
                            cluster: grpc_server
                            max_grpc_timeout: 0s
                        - match:
                            prefix: "/"
                          route:
                            cluster: static_content_server
                http_filters:
                  - name: envoy.grpc_web
                  - name: envoy.router
  clusters:
    - name: grpc_server
      connect_timeout: 0.25s
      type: logical_dns
      http2_protocol_options: {}
      lb_policy: round_robin
      hosts: [{ socket_address: { address: localhost, port_value: 9090 }}]
    - name: static_content_server
      connect_timeout: 2s
      type: logical_dns
      # the dns_lookup_family is required
      dns_lookup_family: V4_ONLY
      lb_policy: round_robin
      hosts: [{ socket_address: { address: localhost, port_value: 8081 }}]

I found documentation about interceptors, which have been added as of v1.1.0, here: https://grpc.io/blog/grpc-web-interceptor/ . And an example of an auth interceptor here: https://nicu.dev/posts/typescript-grpc-web-auth-interceptor .

You'll also need to allow the Authorizaton header through your proxy, like:

              cors:
                allow_origin_string_match:
                - prefix: "*"
                allow_methods: GET, PUT, DELETE, POST, OPTIONS
                allow_headers: keep-alive,..others..,x-grpc-web,grpc-timeout,authorization

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jmzwcn picture jmzwcn  路  6Comments

ivan-sysoi picture ivan-sysoi  路  5Comments

sirudog picture sirudog  路  3Comments

aberasarte picture aberasarte  路  6Comments

NiNJAD3vel0per picture NiNJAD3vel0per  路  3Comments