esp-v2 icon indicating copy to clipboard operation
esp-v2 copied to clipboard

Best practices for Auth on the backend

Open DazWilkin opened this issue 3 years ago • 6 comments

I'm wondering how to implement authentication in a Cloud Endpoints backend on the principle that I don't want to make the implementation specific to Endpoints.

I'm using gRPC and Cloud Run.

My current, naive implementation uses a gRPC interceptor and this expects x-endpoint-api-userinfo in the call's metadata (obtained from the context).

I've configured Firebase Authentication and Google Identity Tokens. The latter to short-circuit my workflow and provide me an easier way to authenticate requests through Endpoints.

However, I'm thinking that it would be better for the Interceptor to look for Cloud Endpoints specific metadata (?) to authenticate and, if not found, fall back to authenticating using the Authorization header (assuming this means the service is running on e.g. Cloud Run without Endpoints, for testing) and, if that's not found, permit without authentication (assuming the service is running locally).

Under this configuration, I must always ensure I deploy the backend to Cloud Run with --no-allow-unauthenticated but that should always be the case and it means the service is loosely-coupled to Endpoints.

Is this approach reasonable? What (other) flaws am I overlooking?

Is there a set of documented best practices for Cloud Endpoints?

DazWilkin avatar Jun 24 '21 01:06 DazWilkin

You mentioned two separate requirements:

  1. Where your application is deployed, with or without Cloud Endpoint
  • use x-endpoint-api-userinfo with cloud endpoints
  • use authorization without cloud endpoint
  • not to check authorization in local
  1. only allow ESP to call your application by using flag --no-allow-unauthenticated

It seems that they are specific to your requirements. I don't think we have any better practices to offer.

qiwzhang avatar Jun 24 '21 01:06 qiwzhang

Thanks for the always prompt and thoughtful replies!

Is my approach reasonable? Are those headers appropriate signals for e.g. "proxied by Cloud Endpoints"? Does this approach too-easily fail-unsafe?

It feels that there are general best practices here:

  • Don't bind backend services to Cloud Endpoints' headers
  • Use "Authorization" header as backup (!) to Cloud Endpoints' headers
  • Don't require authN in your backend (in part this complicates testing)

The documentation for Cloud Endpoints is comprehensive and accurate (well done!) but - as is common with Google -- there's an assumption that everyone is as smart as a Google engineer and there's a lack of documentation for those of us that's aren't.

I think, even if only "guard rails", this could be helpful to other developers.

DazWilkin avatar Jun 24 '21 01:06 DazWilkin

I don't see issues with 2). But I do see some issues with 1):

  • If you know how to verify "Authorization" token in your application, just do it, don't use Endpoint to do it. x-endpoint-api-userinfo could be spoofed.
  • never allow a request without "authorization", it is better for your local testing to generate one.

qiwzhang avatar Jun 24 '21 03:06 qiwzhang

My 2 cents: The goal of a ESPv2 (and API Proxies in general) is to reduce complexity in your API backend. You move common functionality (admission control, rate limiting, observability, authn, authz, etc.), to the proxy. Your API backends can then be simple, you don't need to reinvent the wheel for every backend you write.

Your proposal is to re-implement some authentication and authorization checks in your backend, and then expose your API backend so anyone can call it. IMO this defeats the goal of API proxies and increases the security risk for your API backend. For example:

  • Will you fully integration test both request paths (with ESPv2 and without ESPv2) with all positive and negative cases? Like @qiwzhang mentioned above, your proposal has an edge case where a request that does not go through ESPv2 can send whatever x-endpoint-api-userinfo header it wants. This will result in auth bypass, because your backend assumes ESPv2 authenticated the request already.
  • ESPv2 supports rate limiting. If needed, do you have a plan to re-implement this in your backend? Otherwise you are opening up your backend for unlimited QPS of traffic (potential DoS attack).
  • ESPv2 has generates API-level metrics and access logs for requests that flow through it. Will you have a similar component in your API backend? Otherwise, you won't know which public clients are using it without ESPv2 on the request path.
  • ... etc.

I do understand your concern about platform lock-in. But you are making a tradeoff in terms of simplicity and security. Given that ESPv2 is an open-source proxy that can run on any platform and that Cloud Endpoints has very generous pricing, I would prefer to go with the simple and secure approach :)

nareddyt avatar Jun 24 '21 15:06 nareddyt

Every opinion is helpful, thank you!

I'm genuinely trying to understand this and to keep my life simpler.

Rereading my initial question, I realize I could have worded it more precisely. I'm looking to outsource authentication (AuthN) entirely but I want to handle authorization (AuthZ) consistently, regardless of which authentication approach has been deployed.

You make a good point about the portability of ESPv2; I'd not considered that and should.

I'm not trying to reimplement authentication and -- unless I'm mistaken -- I must implement authorization since that isn't a capability of Cloud Endpoints.

My premise is: If Cloud Endpoints manages authentication for me then my backend should not handle authentication. If my backend does not handle authentication then I should run it without authentication and I can test it without authentication (to test the non-auth functionality).

I will always deploy the backend to Cloud Run with --no-allow-unauthenticated and then the Cloud Run proxy (for there is yet another proxy) still, always authenticates requests... Cloud Endpoints authenticates using a service accounts and for testing, as a project member, I can simply Authorization: Bearer $(gcloud print-identity-token) to obtain a Google Identity Token that will pass authentication.

This makes life easier and my backend only then uses the JWT for authorization. If the user gets to my service, they are authenticated. The remaining question is what permissions do they have on the backend service.

Given the above, for testing, I can run the backend locally or deploy to Cloud Run and authenticate painlessly. And, when Cloud Endpoints is in the mix, I can test it authenticating too.

But, I must juggle headers for authorization which I must implement and which I'd like to do so as a filter similar to authentication that I can enable and disable for testing. When it's disabled, everything can be run. When it's enabled it authorizes based on a JWT.

But, Cloud Endpoints provides the JWT|claims using X-API-Endpoint-UserInfo, without Endpoints, Cloud Run provides the JWT|claims as Authorization Bearer (!?). I've not yet tried this but, given the above configuration, I'd like an authorization service that, when enabled, works regardless of whether Cloud Endpoints, Cloud Run or some other auth mechanism is in the picture.

DazWilkin avatar Jun 24 '21 16:06 DazWilkin

I see your confusion, you are mixing up the authentication provided by Cloud Endpoints vs the authentication provided by Cloud Run (when you deploy with --no-allow-unauthenticated).

You configured ESPv2 to work with Firebase Authentication and Google Identity Tokens. ESPv2 will authenticate those end-user tokens that come in the Authorization header. If the token is valid (authentication successful), ESPv2 will fill in X-API-Endpoint-UserInfo. Then your backend can use this for authorization.

Now, you mention you deploy Cloud Run with --no-allow-unauthenticated. Cloud Run is also performing authentication, but it is not authenticating end-user tokens. Cloud Run doesn't know anything about Firebase. Cloud Run can only authenticate Google Identity Tokens for service-to-service authentication and authorization, as described here.

Technically you can use Cloud Run to authenticate end users, but it only works with Google Identity Tokens via Google Sign in. Any other methods (Firebase, Auth0, Okta) will not work with Cloud Run.

So the best practice we recommend is:

  • Deploy ESPv2 as a public service and configure it to authenticate end-user tokens (Firebase, Google ID, Auth0, Okta, etc.).
  • Deploy your backend with --no-allow-unauthenticated: this is for service-to-service authentication between ESPv2 and your backend, not for end-user authentication. End users were already authenticated by ESPv2.
  • In your backend, use the X-API-Endpoint-UserInfo to authorize the end users (authenticated by ESPv2).

If you are ok with the limitation of Cloud Run only authenticating and authorizing Google Identity Tokens, then I can share how to configure ESPv2 to provide no authentication, and have Cloud Run do all authn/authz.

nareddyt avatar Jun 25 '21 21:06 nareddyt