Open jakepoz opened 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.
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.
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
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?