grpc / grpc-web

gRPC for Web Clients
https://grpc.io
Apache License 2.0
8.64k stars 765 forks source link

Documentation for Credentials #351

Open jakepoz opened 5 years ago

jakepoz commented 5 years ago

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:

` /**

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?

stanley-cheung commented 5 years ago

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.

jakepoz commented 5 years ago

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?

johanbrandhorst commented 5 years ago

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.

stanley-cheung commented 5 years ago

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.

jakepoz commented 5 years ago

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.

swuecho commented 5 years ago

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

jakepoz commented 5 years ago

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.

swuecho commented 5 years ago

that is smart! thanks.

swuecho commented 5 years ago

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


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.)

buehler commented 5 years ago

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 :-)

cwackerfuss commented 5 years ago

@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)
acarl commented 4 years ago

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 }}]
mfickett commented 4 years ago

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