11

my setup:

  1. Python, google app engine using endpoints_proto_datastore
  2. iOS, endpoints Obj-C client library generator

Background

I've setup a test Google cloud endpoints api and had it running pretty quickly. It works great, using the test app in the iOS simulator, and using Google's APIs Explorer. The API is currently open to all with no authentication.

I'd like to: setup an API-key or system-credential that can be used by the app to ensure it alone can access the api - with all others being refused.

The method in the Google Endpoints Auth docs (1) is to create an OAuth 2.0 client ID using the Google Developers Console (2). So I have created an ID for an Installed Application of type: iOS. So far so good.

In the app the GTLService Object looks like this...

-(GTLServiceDemogaeapi *)myApiService {
    GTMOAuth2Authentication *auth = [[GTMOAuth2Authentication alloc] init];
    auth.clientID = @"10???????????????????ie.apps.googleusercontent.com";
    auth.clientSecret = @"z????????????3";
    auth.scope = @"https://www.googleapis.com/auth/userinfo.email";

    static GTLServiceDemogaeapi *service = nil;
    if (!service) {
        service = [[GTLServiceDemogaeapi alloc] init];
        service.authorizer = auth;
        service.retryEnabled = YES;
        [GTMHTTPFetcher setLoggingEnabled:YES];
    }
    return service;
 }

On GAE I've specified the (allowed_client_ids and added a user check to the methods...

@endpoints.api(name='demogaeapi', version='v1',
               allowed_client_ids=['10?????????????ie.apps.googleusercontent.com',
               endpoints.API_EXPLORER_CLIENT_ID],
               scopes=[endpoints.EMAIL_SCOPE],
               description='My Demo API')
class MyApi(remote.Service):

    @TestModel.method(path='testmodel/{id}', name='testmodel.insert', http_method='POST')
    def testModelInsert(self, test_model):

        current_user = endpoints.get_current_user()
        if current_user is None:
            raise endpoints.UnauthorizedException('Invalid token.')

The issue

current_user is always None so the method will always raise an exception. It appears that this is a known problem Issue 8848: Google Cloud Enpoints get_current_user API doesn't fill user_id (3) with no prospect of a fix soon.

Options?

  1. Wait until google fix Issue 8848. Can't really, we have a product to release!

EDIT: 15 May 2015 - Google made Issue 8848 status WontFix.

  1. I saw that it may be possible to use an API Key but while I have been able to create one - I have not found a way to enable it on the backend. I also note this method has a big hole where Google's APIs Explorer can defeat it see SO question (4).

  2. Does the @endpoints.api Argument: auth_level, described here (5), provide an answer? I tried using:

    @endpoints.api(name='demogaeapi', version='v1',
           auth_level=endpoints.AUTH_LEVEL.REQUIRED,
           scopes=[endpoints.EMAIL_SCOPE],
           description='My Demo API')
    

    But was able to use the api from the client app without using credentials. So it clearly did not add any authentication.

  3. Add a hiddenProperty to the client query holding a shared secret key. As described by bossylobster here (6) and Carlos here (7). I tried this but cannot see how to get at the raw request object (subject of another question How to get at the request object when using endpoints_proto_datastore.ndb?).

    @TestModel.method(path='testmodel/{id}', name='testmodel.insert', http_method='POST')
    def testModelInsert(self, test_model):
    
        mykey,keytype = request.get_unrecognized_field_info('hiddenProperty')
        if mykey != 'my_supersecret_key':
            raise endpoints.UnauthorizedException('No, you dont!')
    

EDIT: Another option has surfaced:

  1. Create a Service Account using the Google Developers Console (2). Use this account to gain access to the API without the need for user consent (via UI). However, Google appear to limit the number of apps that can be added this way to 15 or 20. See Google OAuth2 doc (8). We will likely exceed the limit.

The Question

Does anyone know how I can get any of these options working? Or should I be approaching this in a different way?

As you can see I'm in need of guidance, help, ideas...


As I need 10 reputation to post more than 2 links: here are the links I had to extract and add as references. Sort of ruined the flow of the question really.

  1. cloud.google.com/appengine/docs/python/endpoints/auth
  2. console.developers.google.com/
  3. code.google.com/p/googleappengine/issues/detail?id=8848
  4. stackoverflow.com/a/26133926/4102061
  5. cloud.google.com/appengine/docs/python/endpoints/create_api
  6. stackoverflow.com/a/16803274/4102061
  7. stackoverflow.com/a/26133926/4102061
  8. developers.google.com/accounts/docs/OAuth2
ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Nigel
  • 471
  • 4
  • 13
  • This is a well-researched question. Thanks. I'm currently reading it over carefully and doing some research, but just saying thanks for the effort of laying out your problem clearly with tons of relevant info. I feel if we can work together to get an answer to this, it might be a great question for future users to refer to, which is what SO is supposed to be for. – Nick Jan 06 '15 at 21:46
  • This is still an open issue for me, our API needs to be secure. I've been busy on a different module and it is time to get back to this. So my current thinking is that option 4, bossy lobster's suggested hiddenProperty, holds the most promise. What are your thoughts? – Nigel Jan 07 '15 at 10:49
  • I also ran into this problem and would be (much) interested in an answer. At this point I am using a workaround whereby the client passes an API key (a random string) at the application level (over HTTPS) and the service checks it also at the application level. – Drux Feb 28 '15 at 18:39
  • Have you tried the workarounds in the answer at http://stackoverflow.com/questions/16661109/google-endpoints-api-chrome-extension-returns-none-for-endpoints-get-current-u/16661765#16661765 ? – Alex Martelli Mar 12 '15 at 03:59
  • Alex - now that looks interesting, I'll have to read through it a few times to get to grips with the options. We ended up changing the architectural concept for our product and this resulted in dropping the need for endpoints. – Nigel Mar 12 '15 at 17:37
  • @Nigel, tx for the follow-up, hadn't seen it because you didn't use the @ - myuserid mention so StackOverflow didn't know you were replying to me so didn't let me know:-). So could you perhaps please post a self-answer one way or another, and accept it, so this long-standing, high-upvoted question can finally stop being "unanswered"...? TIA!-) – Alex Martelli Mar 21 '15 at 02:04
  • Too bad you couldn't get this to work. I'm hoping I don't run into the same problem when I go to access my API from a Dart client. BTW, I haven't seen auth scope like that. In a Python client it is specified as 'https://www.googleapis.com/auth/userinfo.email'. – Tom Russell Aug 12 '16 at 05:59

1 Answers1

1

An excellent question and a real problem. I solved it by having my own "user" database table. The Long key (I use Java) is auto-generated on write and that is the value I use to uniquely identify the user from then on. Also written to that table are the Google ID (provided if being called through a web page), email address, and iOS id (when using the iOS app while not signed-in to a Google account).

When my AppEngine endpoint gets called, I use information that is present in the User parameter (just the authenticated email address for signed-in clients) or a separate, optional iosid parameter in order to fetch my internal userid number from said table and use that from then on to uniquely identify my user.

I use the same table to store other user-specific information, too, so it really isn't any different than trying to use current_user as a primary key other than I had to make three fields (googleid, email, iosid) indexed for lookup.

Brian White
  • 8,332
  • 2
  • 43
  • 67